skip to content

How does CPython choose sys.path[0] for a script, -m, -c and the REPL?

level: middleimportance: should knowfreq 45%

answer

  1. It depends how you started the interpreter
  2. Script directory, working directory, or empty string
  3. Empty string is re-resolved at each import
  4. Prepended ahead of PYTHONPATH and the stdlib
  5. sys.flags.safe_path when no entry is added

basics

~20 s

CPython prepends the script's own directory for python app.py, the absolute working directory for -m, and the empty string for -c, piped stdin and the interactive REPL. The empty string means the working directory at import time.

solid answer

~40 s

The first entry is chosen from how the interpreter was started, and it is always prepended ahead of `PYTHONPATH` and the standard library. `python app.py` gives the absolute directory containing `app.py`; `python -m pkg.cli` gives the absolute current working directory, because a module located by name has no script directory; `python -c`, a script piped on stdin, and the interactive REPL all give `''`. That empty string is not a fixed directory — the path importer resolves it against the process's current working directory at each import, so an `os.chdir` moves it, while an absolute entry stays put. Starting with `-P`, or with `PYTHONSAFEPATH` set to a non-empty value, adds no such entry at all and sets `sys.flags.safe_path` to True.

code

console · 5 lines
console
printf 'import sys\nprint(repr(sys.path[0]))\n' > show.py
python3 show.py                                     # absolute dir of show.py
python3 -c 'import sys; print(repr(sys.path[0]))'   # ''
echo 'import sys; print(repr(sys.path[0]))' | python3   # ''
python3 -P -c 'import sys; print(sys.flags.safe_path)' # True

go deeper

for a junior

Know the common case: running a script makes that script's own directory the first place Python looks for modules. That single fact explains most surprising import results you will hit early on.

for a middle

Be able to give all four outcomes and say that the entry is prepended, ahead of PYTHONPATH and the standard library. The detail that earns the point is that the empty string is re-resolved against the working directory at every import.

for a senior

Demonstrate that you check this on real processes: log sys.path[0] and sys.flags.safe_path at startup, and know that a service which changes directory while running has moved its own first search location.

for a principal

Decide the launch convention for the fleet — whether services start from a module, a file or an inline command, and whether safe path mode is the default — so that the import surface is a property of the platform rather than of each team's start script.

### One list, four ways of filling its first slot `sys.path` is a plain list of strings that the import system searches in order. Everything after the first entry is stable across launch modes — `PYTHONPATH` directories, then the standard library, then `site-packages`. The **first** entry is the interesting one, because CPython chooses it from how you started the interpreter: | how you started it | `sys.path[0]` | |---|---| | `python app.py` | absolute directory containing `app.py` | | `python -m pkg.cli` | absolute current working directory | | `python -c "..."` | `''` | | script piped on stdin | `''` | | interactive REPL | `''` | | `python -P ...` / `PYTHONSAFEPATH=1` | no entry added at all | The rationale is consistency of intent. Running a file means "this file and its neighbours are the program", so its directory goes first. `-m` locates a module by name rather than by file, so there is no script directory to use and the working directory is the closest equivalent. `-c`, stdin and the REPL have neither, so CPython inserts the empty string. ### The empty string is not a directory This is the detail that separates a memorised table from real understanding. `''` is not shorthand for "the directory the process started in". The path importer resolves it against the process's **current** working directory at the moment of each import, so it moves when the process moves: ```python import os, sys sys.path.insert(0, "") # what -c, stdin and the REPL give you for free os.chdir("/some/other/dir") import helper # searched for in /some/other/dir, not where you started ``` An absolute entry — the script directory, or the cwd that `-m` recorded — is frozen at startup and does not follow a `chdir`. The empty entry does. That is the whole reason the empty entry is treated as the higher-risk one: its meaning is controlled by mutable process state. ### Prepend, not append All of these go at the **front**. That ordering is the part candidates get backwards. The assembled order is: implicit first entry, then each `PYTHONPATH` directory in the order given, then the standard-library zip and directory, then `site-packages`. So the first entry outranks not only your installed dependencies but the standard library itself — which is why a stray file can redefine a standard-library name, and why "the interpreter prefers its own modules" is simply false. ### Turning the entry off `python -P`, or `PYTHONSAFEPATH` set to any non-empty value, tells CPython to skip the implicit first entry entirely. There is no placeholder left behind; `sys.path[0]` becomes the standard-library zip and `sys.flags.safe_path` reads `True`: ```console python3 -c 'import sys; print(repr(sys.path[0]), sys.flags.safe_path)' python3 -P -c 'import sys; print(repr(sys.path[0]), sys.flags.safe_path)' ``` Both the flag and the environment variable, along with `sys.flags.safe_path`, arrived in Python 3.11. A trap worth remembering: `PYTHONSAFEPATH=0` **enables** safe path mode, because CPython tests these variables for non-emptiness rather than parsing them as booleans. `PYTHONSAFEPATH=` (empty) is the way to leave it off. Note what `-P` does *not* do. It removes one entry. It does not ignore `PYTHONPATH`, does not skip `site-packages`, and does not make imports "safe" in any broader sense — isolated startup is the mode that additionally ignores the `PYTHON*` variables and the user site directory. ### Where this shows up in practice * A test helper that works under `python -m` and fails as a direct script, or vice versa, is almost always this table: the two modes put different directories first, so a sibling import resolves in one and not the other. * A REPL session that imports "the wrong version" of a module is usually the empty entry picking up whatever is in the directory the shell happened to be in. * A daemon that changes directory during work and imports lazily afterwards has moved its own first entry into whatever it chdir'd to — a security problem, not just a confusing one. * Reading `sys.path[0]` and `sys.flags.safe_path` at startup and logging both is the cheapest possible diagnostic, and the only proof of what a deployed process actually searched. ### How to answer Name the launch mode as the deciding input, give the three outcomes (script directory, working directory, empty string), and then volunteer the part that shows depth: the empty string is re-resolved on every import, so it follows the process's working directory rather than pinning the startup one.

  • Is the empty first entry the same as the directory the process started in?
    No, and that is the point of it. It is not resolved once at startup: the path importer treats `''` as the current working directory at the moment of each import, so a later `os.chdir` changes where subsequent imports are found. If you need the start directory pinned, capture `os.getcwd()` early and put that absolute string on the path yourself. An absolute entry, like the one a script or `-m` produces, does not move.
  • Why do `python -m pkg.cli` and `python pkg/cli.py` resolve sibling imports differently?
    Because they prepend different directories. Running the file directly prepends the directory containing `cli.py`, so `pkg`'s siblings are importable but `pkg` itself may not be. `-m` locates the module by name and prepends the absolute working directory instead, so `pkg` is importable as a package. Most "works one way, fails the other" import bugs are exactly this difference, not a packaging problem.
  • How do you tell at runtime which mode a deployed process is in?
    Read `sys.path[0]` and `sys.flags.safe_path` at startup and log both. An empty first entry means `-c`, piped stdin or the REPL; an absolute first entry is either the script directory or the working directory recorded by `-m`; and `safe_path` being True means no implicit entry was added at all and `sys.path[0]` is the standard-library zip. That log is the only real evidence of what the process searched.

saying these in an interview costs you the question

  • Says sys.path[0] is always the current working directory
  • Treats the empty string as a literal directory name
  • Believes PYTHONPATH is searched before sys.path[0]
  • Thinks -m and a direct script path prepend the same entry
  • Assumes a chdir cannot change where later imports resolve

context