What role does __main__.py play when Python runs a package, a directory or a .zip file?
answer
- A filename convention, not a keyword
- The interpreter needs one file to start
- Two different mechanisms, one filename
- A path entry is not an imported package
basics
~20 smain.py is what the interpreter executes when the target is not a single .py file. With -m the package is imported first and its main.py runs as the main module; a directory or zip path is prepended to sys.path and its top-level main.py is executed.
solid answer
~40 s`python -m catalogue` imports the package normally — running `catalogue/__init__.py` — and then executes `catalogue/__main__.py` under the name `__main__`, with `__package__` set to the package name. A package without that file cannot be executed at all: the interpreter reports that there is no module `catalogue.__main__` and that a package cannot be directly executed. Pointing the interpreter at a directory or at a zip archive is a different mechanism with the same filename convention: the container is prepended to `sys.path` and its top-level `__main__.py` is executed, with no package context and no `__init__.py` involved — which is exactly how a single-file zipped Python application runs. A plain module needs none of this: `python -m catalogue.loader` runs `loader.py` itself.
code
python · 18 linesimport pathlib
import subprocess
import sys
import tempfile
import zipfile
with tempfile.TemporaryDirectory() as tmp:
app = pathlib.Path(tmp, "catalogue_app")
app.mkdir()
app.joinpath("__main__.py").write_text("print('catalogue importer starting')\n")
print(subprocess.run([sys.executable, str(app)],
capture_output=True, text=True).stdout.strip())
archive = pathlib.Path(tmp, "catalogue_app.zip")
with zipfile.ZipFile(archive, "w") as handle:
handle.write(app / "__main__.py", "__main__.py")
print(subprocess.run([sys.executable, str(archive)],
capture_output=True, text=True).stdout.strip())go deeper
Recall that -m on a package looks for a file named main.py inside it, which is the same convention that lets a directory or zip archive be handed to the interpreter.
Explain both mechanisms: under -m the package is imported and main.py executed as a submodule, while a directory or zip is a sys.path entry whose top-level main.py runs with no package context.
Judge what belongs in that file: keep it to a call into the package so it is cheap to execute, safe to re-execute in a worker, and never a second home for classes or caches.
Decide how the organisation exposes runnable code, weighing a package main.py against installed console commands, and keep the two consistent so operators have one predictable way to start anything.
### One convention, three containers `__main__.py` is the file the interpreter executes when the thing you point it at is not a single `.py` file. It shows up in three places, and the mechanism is slightly different in each. **A package run with `-m`.** `python -m catalogue` imports the package `catalogue` normally — executing `catalogue/__init__.py` — and then executes the submodule `catalogue.__main__` under the name `__main__`. If the package has no such file, the interpreter refuses with a precise message: ```console $ python -m email /usr/bin/python: No module named email.__main__; 'email' is a package and cannot be directly executed ``` A *module* needs no `__main__.py` at all: `python -m catalogue.loader` runs `loader.py` itself as the main module. `__main__.py` exists so that a whole package can present one obvious entry point — which is why so much of the standard library is invoked this way (`python -m venv`, `python -m http.server`, `python -m zipapp`). **A directory path.** `python catalogue_app/` does *not* import the directory as a package. The directory is prepended to `sys.path` and its top-level `__main__.py` is executed as the main module. Any `__init__.py` inside is irrelevant; the modules sitting next to `__main__.py` become importable as **top-level** modules, because their directory is `sys.path[0]`. **A zip path.** `python catalogue_app.pyz` works the same way: the archive itself is prepended to `sys.path` — CPython can import from zip archives — and the `__main__.py` stored at the archive root is executed. This is the whole trick behind single-file Python applications: a zip with a top-level `__main__.py`, the pure-Python code it needs alongside it, and optionally a shebang line prepended so the file is directly executable. ### What each form sets Under `-m` the module runs with `__package__` set to the package name, so the package it belongs to is fully imported and available. Under the directory and zip forms there is no enclosing package at all — the container is a path entry, not a package — so `__package__` is empty and relative imports from `__main__.py` have nothing to resolve against; import its siblings as top-level modules instead. In every case `__name__` is `"__main__"`, so the usual guard idiom still applies. In practice a `__main__.py` rarely needs the guard for itself, because nothing imports it under that name — but write it anyway if the file will be re-executed by worker processes, and keep the file thin either way: ```python # catalogue/__main__.py from catalogue.loader import main raise SystemExit(main()) ``` Keeping the entry file free of definitions and state is the habit that prevents a second, separate copy of your classes and caches when the same file is also imported by name. ### Underneath: `runpy` All three forms go through the `runpy` module. `runpy.run_module(name, run_name="__main__")` is what `-m` uses; `runpy.run_path(path)` is what handles a script, a directory or a zip. Knowing this makes the behaviour predictable rather than magical: `runpy` locates the code, builds a fresh module namespace, sets `__name__` (and `__package__`, `__file__`, `__spec__`) in it, and executes. ### Common mistakes * Expecting `__init__.py` to be the entry point. It is executed under `-m` as part of importing the package, but it is not what runs as `__main__`, and for a directory or zip run it is not consulted at all. * Expecting every package to be executable. Most are not; the error message above is the tell. * Putting real logic in `__main__.py`. It is the one module most likely to be duplicated or re-executed, so it should contain a call and nothing else. * Assuming a zip must be unpacked first. It does not: the interpreter imports directly from the archive, which is why the single-file form works at all. ### How to answer it in an interview Say what the file is for in one sentence — the entry point the interpreter runs when the target is a package, a directory or a zip rather than a `.py` file — then distinguish the two mechanisms: under `-m` the package is imported first and `__main__.py` is run as a submodule, while a directory or zip is simply prepended to `sys.path` and its top-level `__main__.py` executed with no package context.
- Does python -m catalogue also run catalogue/__init__.py?Yes. `-m` resolves the target through the import system, so the package is imported first and its `__init__.py` executes, then `catalogue/__main__.py` runs under the name `__main__`. That is why anything the package's `__init__` sets up is available to it, and why import-time side effects in `__init__.py` are paid even for a trivial command-line run.
- What happens when you point python at a package that has no __main__.py?It refuses before running anything, reporting no module named `<pkg>.__main__` and that the package cannot be directly executed. Most packages are not executable; `__main__.py` is an opt-in that a project adds when it wants one obvious command-line entry point, and a single module inside the package can still be run with `-m` without it.
- Does running a directory import it as a package?No. The directory is prepended to `sys.path` and its top-level `__main__.py` is executed; any `__init__.py` inside is ignored, and there is no enclosing package, so `__package__` is empty and relative imports have nothing to resolve against. Modules beside `__main__.py` are importable as top-level modules because their directory is now `sys.path[0]`.
saying these in an interview costs you the question
- Says __init__.py is the file that runs under -m
- Assumes every package can be executed with -m
- Believes a zip must be unpacked before Python runs it
- Thinks running a directory imports it as a package
- Puts substantial program logic in __main__.py