skip to content

How does python -m catalogue.loader differ from python catalogue/loader.py in what it puts on sys.path[0]?

level: middleimportance: should knowfreq 46%

answer

  1. One form is import-driven, one path-driven
  2. Ask which directory is prepended
  3. Working directory versus the script's own directory
  4. Only one form imports the parent package

basics

~20 s

The -m form puts the current working directory at the front of sys.path, so the package being run stays importable. The script-path form puts the script's own directory there instead, so the parent package is not on the path at all.

solid answer

~40 s

`python -m catalogue.loader` resolves the module through the normal import system, so `sys.path[0]` is the current working directory: the parent package `catalogue` is imported first — running its `__init__.py` — and the module then executes as `__main__` with `__package__` set to `"catalogue"`. `python catalogue/loader.py` instead prepends the *script's* directory, so `catalogue` itself is not importable, `__init__.py` never runs, `__package__` is `None`, and the modules beside the script look like top-level modules. That single difference is behind most "works one way, ImportError the other" reports. Since 3.11 the `-P` option and the `PYTHONSAFEPATH` environment variable suppress that first entry entirely, so only installed code is importable.

code

python · 6 lines
python
import sys

print("__name__     =", __name__)
print("__package__  =", __package__)
print("sys.path[0]  =", sys.path[0])
print("sys.argv[0]  =", sys.argv[0])

go deeper

for a junior

Recall that -m takes a dotted module name resolved through sys.path while the other form takes a file path, and that the two differ in what ends up importable.

for a middle

Explain both sys.path[0] values, that package is set only under -m, and why running a file inside a package by path breaks that file's own package imports.

for a senior

Demonstrate the diagnosis: when a job runs locally but fails in a container, compare sys.path[0] under each launch form, and standardise on -m from a fixed working directory or on an installed console command.

for a principal

Set the policy for how services and jobs are started across repos, including whether safe path is on, so behaviour does not depend on which directory an operator happened to be in.

### Two launch forms, two different first entries on `sys.path` The interpreter puts one directory at the front of `sys.path` before your code runs, and *which* directory depends on how you launched it: | launch | `sys.path[0]` | `__package__` | `sys.argv[0]` | |---|---|---|---| | `python catalogue/loader.py` | the directory holding the script (`.../catalogue`) | `None` | the path you typed | | `python -m catalogue.loader` | the current working directory | `"catalogue"` | the module's full file path | | `python -c "..."` or stdin | `""`, meaning the working directory | `None` | `"-c"` | Everything else about the two runs is the same: the module executes under the name `__main__`, the same interpreter and the same site-packages are in play. The one-line difference in `sys.path[0]` is what makes the two forms behave so differently. ### Why the script form breaks package-internal imports Suppose the layout is `/srv/app/catalogue/{__init__.py, loader.py, records.py}`. Run `python catalogue/loader.py` from `/srv/app`, and `sys.path[0]` is `/srv/app/catalogue`. From there, `records` is importable as a **top-level** module, but `catalogue` is not importable at all — its parent directory is not on the path — so `import catalogue.records` fails with `ModuleNotFoundError`. The package's `__init__.py` never runs, and `__package__` is `None`, so the module has no package context of its own. Run `python -m catalogue.loader` from `/srv/app`, and `sys.path[0]` is `/srv/app`. The interpreter now resolves `catalogue.loader` through the ordinary import system: it imports the parent package `catalogue` first — executing `catalogue/__init__.py` — and only then executes `loader.py` under the name `__main__`, with `__package__` set to `"catalogue"`. Both `import catalogue.records` and the package's own machinery work, because the module is being run *inside* its package rather than beside it. Two smaller differences fall out of the same mechanism. Under `-m` the module has to be **findable on `sys.path`**, so `python -m catalogue.loader` from the wrong directory raises `ModuleNotFoundError` instead of quietly running the wrong file. And `sys.argv[0]` is rewritten to the module's resolved file path under `-m`, while the script form leaves exactly the string you typed — worth knowing if you log or parse it. ### What `-m` is doing underneath `-m` is implemented with the `runpy` module: it locates the module through the normal import system, then executes its code in a fresh namespace whose `__name__` is `"__main__"`. `runpy.run_module` and `runpy.run_path` expose the same machinery to your own code. This is why `-m` and an import share a search path but produce a module under a different name — a fact with consequences of its own when a module is both run and imported in the same process. ### The safe-path escape hatch Prepending the script directory or the working directory is a convenience with a cost: a local file can end up being found before something you expected from the environment. Since **3.11** the interpreter accepts the `-P` option and the `PYTHONSAFEPATH` environment variable, which suppress that prepend entirely; `-I` (isolated mode) implies it along with ignoring the user site directory. With safe path on, `python -m catalogue.loader` no longer finds `catalogue` in the working directory — only installed code is importable, which is exactly what you want for a reproducible run and exactly what surprises you the first time you enable it. ### Practical guidance * Run anything that lives inside a package with `-m` from the project root, or through the console command that installing the project creates. Reserve the script-path form for genuinely standalone single files. * When somebody reports "it works when I run it as a script but not with `-m`" (or the reverse), ask what `sys.path[0]` is in each case; the answer is almost always that one form makes the package importable and the other does not. * In a container image or CI job, `-m` from a fixed working directory is more predictable than a path, because it fails loudly when the package is missing rather than executing a file that happens to be there. ### How to answer it in an interview Give the two `sys.path[0]` values first — script directory versus working directory — then the two consequences that follow: the package's `__init__.py` runs only under `-m`, and `__package__` is set only under `-m`. Mentioning `-P` / `PYTHONSAFEPATH` and `runpy` shows you know the mechanism rather than the folklore.

  • Why does the -m form execute the package's __init__.py before your module?
    Because `-m` goes through the ordinary import system, and importing `catalogue.loader` requires importing the parent package `catalogue` first. That is what makes `__package__` meaningful and package-internal imports work. The script-path form never imports the package at all — it just executes a file — so `__init__.py` is not involved and any package context is absent.
  • What do the -P option and PYTHONSAFEPATH change here?
    Added in 3.11, both stop the interpreter from prepending the script's directory or the working directory to `sys.path`. Only installed code and the standard library remain importable, which makes a run reproducible and prevents a stray local file from being picked up. The cost is that `python -m yourpkg` from a source checkout stops working unless the project is installed.
  • How do the two forms differ in sys.argv[0]?
    The script form leaves exactly the string you typed, relative path and all. Under `-m` the interpreter rewrites `sys.argv[0]` to the resolved filesystem path of the module it found. If you log the invocation, derive a program name from it, or compare it against a path, that difference is worth knowing before it surprises you.

saying these in an interview costs you the question

  • Says both forms put the same directory on sys.path
  • Thinks -m walks the filesystem instead of searching sys.path
  • Believes -m skips the package __init__.py
  • Assumes the working directory is always importable
  • Confuses sys.path with the PATH environment variable

context