How does python -m pkg.mod differ from running python pkg/mod.py?
answer
- Two entries, two different first path entries
- One resolves a name, one reads a file
- Only one of them imports the parent package
- Package context decides whether relative imports work
- Script directory versus current working directory
basics
~20 sPython's -m switch resolves a dotted module name through the import system, imports the parent package, and puts the working directory first on sys.path. A file path runs that file with its own directory first and no package context, so relative imports fail.
solid answer
~40 sBoth entries execute their target as `__main__`, but they arrive differently. `python -m pkg.mod` looks the name up on `sys.path`, imports `pkg` (running its `__init__.py`), then executes `pkg/mod.py` with `__package__` set to `"pkg"` and `__spec__` populated — so relative imports resolve — and puts the **current working directory** at `sys.path[0]`. `python pkg/mod.py` executes that file with `__package__` and `__spec__` empty, so any relative import raises `ImportError: attempted relative import with no known parent package`, and it puts the **script's own directory** at `sys.path[0]`, which changes what the file's plain imports can even see. `sys.argv[0]` differs too: the path you typed for a script, the module's resolved file path under `-m`. `python -m pkg` runs the package's `__main__.py`.
code
python · 7 linesimport sys
print("__name__ :", __name__)
print("__package__:", __package__)
print("__spec__ :", None if __spec__ is None else __spec__.name)
print("sys.argv[0]:", sys.argv[0])
print("sys.path[0]:", sys.path[0])go deeper
Recall that -m takes a dotted module name and a script run takes a file path, and that the relative-import error you hit when running a file inside a package usually disappears when you enter through -m instead.
Explain both mechanisms side by side: name resolution through sys.path versus reading a file, working directory versus script directory at sys.path[0], and package and spec populated only under -m.
Show the operational consequences: shadowed imports and path-dependent failures between local runs and deployed runs, the duplicate-module trap when the -m target is imported again by dotted name, and PYTHONSAFEPATH as hardening.
Decide the convention: one documented entry point per runnable component, a thin main.py over an importable main(), and no code that derives locations from sys.argv[0], so every environment starts the program the same way.
## Two doors into the same file Both commands end with one module bound to `"__main__"`, but everything around that module is assembled differently, and the differences are exactly the ones that make code "work on my machine". ### 1. How the target is found `python pkg/mod.py` takes a **filesystem path**. The interpreter reads that file, compiles it, and runs it. Nothing is imported to get there; the file need not be inside a package, need not be on `sys.path`, and its name need not be a valid identifier. `python -m pkg.mod` takes a **dotted module name**. The import system resolves it against `sys.path` — which means it can find a module inside the current project, inside a virtual environment's installed distributions, or anywhere else on the path. Resolving `pkg.mod` first imports `pkg`, executing its `__init__.py` and all its side effects, and only then executes `mod` under the run name `"__main__"`. If the name resolves to a *package* rather than a module, as in `python -m pkg`, the package's `__main__.py` is what runs. ### 2. What lands first on sys.path This is the difference that bites hardest. - Script path: `sys.path[0]` is the **directory containing the script**, as an absolute path. - `-m`: `sys.path[0]` is the **current working directory**, as an absolute path. So `python pkg/mod.py` makes `mod.py`'s siblings importable as top-level modules while the project root may be invisible; `python -m pkg.mod` makes the project root importable while `pkg`'s siblings are only reachable through the package. A file that imports a project module happily under one entry raises `ModuleNotFoundError` under the other, and a file sitting next to a module that shadows a standard-library name will silently shadow it under the script entry. Both prepends can be switched off with the `PYTHONSAFEPATH` environment variable (or its command-line equivalent), added in Python 3.11, which is the right hardening for a program that must not pick up modules from whatever directory it happens to be started in. ### 3. Package context Under `-m` the executed module keeps a real import identity: `__package__` is `"pkg"` and `__spec__` is a populated spec whose `name` is `"pkg.mod"` — even though `__name__` is `"__main__"`. Relative imports are resolved against `__package__`, so `from . import client` works. Run the same file by path and `__package__` is `None`, `__spec__` is `None`, and that relative import raises `ImportError: attempted relative import with no known parent package`. This is the single most common reason a developer discovers `-m` at all. There is no flag that fixes it; the fix is to enter through `-m`, or to keep the runnable file outside the package. ### 4. sys.argv and __file__ `sys.argv[0]` is the path exactly as typed for a script run, and the resolved absolute file path of the module under `-m`. Everything after it is identical in both cases, so a program that only reads `sys.argv[1:]` is unaffected — but a program that derives a data directory from `sys.argv[0]` will behave differently under the two entries. Deriving it from `__file__` is the stable choice. ### 5. The duplicate-module hazard Under `-m`, the module is in `sys.modules` under `"__main__"` — not under `"pkg.mod"`. If any other module then does `import pkg.mod`, the import system finds no cached entry and executes the file a **second** time, producing a second module object with its own globals. Now there are two copies of every module-level constant, registry, cache and class object; `isinstance` checks against the "same" class fail, and a module-level list meant as a shared collector exists twice, so half the appends land in a collection nobody reads. The standard defence is to keep the entry module thin — a `__main__.py` (or a tiny script) that imports the real module and calls its `main()` — so the substantial code is only ever loaded under its real name. ### Choosing between them Use `-m` for anything that lives inside a package, anything that uses relative imports, and anything installed into an environment; it is also how you run a stdlib tool module without knowing where it lives on disk. Use a plain script path for a standalone file with no package around it. When both are possible, `-m` is the more predictable entry, because the module is found the same way every other import is found rather than by where the file happens to sit.
- Why does a relative import fail when the file is run by path but work under -m?Relative imports are resolved against `__package__`. Under `-m` the import system resolved a dotted name, so `__package__` is the parent package and `__spec__` is populated. A file run by path was never imported, so both are empty and there is no anchor to resolve `from .` against — hence `ImportError: attempted relative import with no known parent package`.
- What does python -m pkg execute when pkg is a package rather than a module?It imports `pkg` — running `__init__.py` — and then executes `pkg/__main__.py` under the run name `"__main__"`. That file is the package's command-line entry point, and it is why `python -m somepackage` works for many installed tools without any console script installed on `PATH`.
- How can one module end up loaded twice in a process started with -m?The `-m` target is cached in `sys.modules` as `"__main__"`, not under its dotted name. A later `import pkg.mod` therefore misses the cache and executes the file again, creating a second module object with separate globals — duplicate classes, duplicate registries, failing `isinstance` checks. Keep the entry module thin so the real module is only ever imported under its real name.
saying these in an interview costs you the question
- Says -m and a script path are interchangeable
- Thinks -m puts the module's own directory first on sys.path
- Believes relative imports work in a file run by path
- Assumes -m skips the parent package's __init__.py
- Confuses -m, which takes a module name, with -c, which takes source
- Passes a .py path to -m and expects it to work