skip to content

How does CPython assemble sys.path when the interpreter starts?

level: middleimportance: should knowfreq 48%

answer

  1. It is built in a fixed order
  2. Invocation decides the first entry
  3. An environment variable comes before the stdlib
  4. The site module appends third-party locations
  5. site-packages .pth lines can add or execute

basics

~10 s

In order: an entry for the script's directory or the current directory, then PYTHONPATH entries, then the interpreter's own standard-library directories, then the site-packages directories that the site module appends while processing .pth files.

solid answer

~40 s

`sys.path` is a plain list of strings built during startup and searched in order. Entry zero comes from how you invoked Python: the script's directory for `python app.py` (absolute since 3.11), and `''` — the current working directory — for the REPL and `-c`. Next come the directories from the `PYTHONPATH` environment variable, then the interpreter's own standard-library locations derived from its prefix, then whatever the `site` module appends: the user site directory and the environment's site-packages, plus any directory named by a `.pth` file found there. Startup flags change the result: `-S` skips `site` entirely, `-s` drops the user site directory, `-E` ignores `PYTHON*` variables, and `-P` or `PYTHONSAFEPATH=1` suppresses the leading script or current-directory entry. Afterwards it is just a list, and code may mutate it at runtime.

code

python · 7 lines
python
import sys
import sysconfig

for entry in sys.path:
    print(entry or "<current directory>")

print("site-packages:", sysconfig.get_path("purelib"))

go deeper

for a junior

Know that sys.path is an ordered list of directories, that your script's own directory is on it, and that site-packages holds installed code. Being able to print sys.path and read it is the expectation here.

for a middle

Explain the assembly order end to end — invocation entry, PYTHONPATH, interpreter directories, site-packages appended by the site module — and name the flags that change it, such as -S, -s and -E.

for a senior

Demonstrate using the order as a diagnostic: compare sys.path between two invocations to explain why the wrong thing was imported, and know what .pth files and runtime path mutation do to reproducibility.

for a principal

Own the policy: whether the working directory is importable at all, whether PYTHONPATH is allowed in deployment, and how -P or isolated mode makes the import surface of a production process something you can state rather than discover.

## sys.path is a list, and its order is the whole story `sys.path` is nothing more exotic than a list of strings that the import machinery walks front to back when it needs to locate a top-level module or package. Everything interesting about it is *how it gets populated* and *in what order*, because the first match wins and later entries never get a say. ### 1. The leading entry — where you started from The first entry is decided by how the interpreter was invoked: * `python app.py` — the directory containing `app.py`. Since 3.11 CPython inserts it as an absolute path, so a later change of working directory cannot silently change its meaning. * The REPL, and `python -c "..."` — `''`, an empty string meaning "the current working directory, resolved at the moment of each import". * Reading a program from standard input behaves like the interactive case. This entry is the reason your own modules are importable without installing anything, and equally the reason a local file can shadow a standard-library name. You can switch it off. Since 3.11, the `-P` flag and the `PYTHONSAFEPATH=1` environment variable both tell CPython not to prepend that leading entry at all, which is a useful hardening step for a process that should import only what has been installed for it. ### 2. PYTHONPATH Next come the directories listed in the `PYTHONPATH` environment variable, split on the platform's path separator. They sit ahead of the standard library, which is what makes `PYTHONPATH` powerful and dangerous in equal measure: an entry there can shadow standard-library and installed code for every process that inherits the variable. `-E` makes the interpreter ignore `PYTHON*` variables entirely, and `-I` (isolated mode) implies `-E` plus dropping the user site directory. ### 3. The interpreter's own directories Then come the installation-dependent locations derived from the interpreter's prefix: a zip archive of the standard library if one is present, the standard-library directory itself, and the directory holding compiled extension modules. `PYTHONHOME` overrides the prefix used to compute these, which is almost always a mistake outside of embedding scenarios — pointing it at a mismatched installation is a reliable way to produce an interpreter that cannot find its own standard library. ### 4. site-packages, appended by the site module Unless the interpreter was started with `-S`, CPython imports the `site` module at the end of startup, and `site` appends the third-party locations: the per-user site directory (skipped with `-s`, or `PYTHONNOUSERSITE`, and reported by `site.ENABLE_USER_SITE`) and the environment's site-packages, which `site.getsitepackages()` will report back to you. While scanning those directories, `site` processes every `.pth` file it finds there. The rule is small and worth knowing exactly: each line that names a directory is appended to `sys.path`; each line that begins with `import ` is *executed* as Python code. That second rule is not a curiosity — it is the hook that editable installs and several path-manipulating tools rely on, and it means anything writable in site-packages can run code at every interpreter start. A virtual environment is not a separate mechanism: its marker file makes the interpreter compute a different prefix, so `site` appends the environment's site-packages rather than the base installation's, and the ordering above is unchanged. ### After startup Once startup is over, `sys.path` is an ordinary mutable list. `sys.path.append(...)` and `sys.path.insert(0, ...)` work, and plenty of code does exactly that to make a sibling directory importable. Treat it as a smell: it is invisible to tooling, it is order-sensitive, it runs after some imports have already resolved, and it is the usual root cause of one file being loaded twice under two names. Installing the project — including as an editable install during development — expresses the same intent in a way every process and every tool agrees on. Two caching details matter when you *do* manipulate the path or the filesystem at runtime. The import system caches a finder per path entry in `sys.path_importer_cache`, and directory listings are cached too. Create a module file after the containing directory has already been searched and the import can fail on stale state; `importlib.invalidate_caches()` is the supported way to force a re-scan. ### Reading the real thing The fastest way to answer "why did it import *that*" is to print the path itself. `python -c "import sys; print(*sys.path, sep='\n')"` shows the assembled order for that exact invocation, and comparing the output between two invocations — with and without a flag, inside and outside an environment — explains almost every path mystery without guesswork.

  • What exactly can a .pth file in site-packages do to sys.path?
    The `site` module reads every `.pth` file it finds in the site directories. A line naming a directory is appended to `sys.path`; a line beginning with `import ` is executed as Python code at interpreter startup. That execution hook is how editable installs and some path-rewriting tools work, and it is why a writable site-packages is effectively code execution at every start.
  • Why is appending to sys.path at runtime considered worse than installing the project?
    Because it is invisible and late. Tooling, subprocesses and other entry points do not see the mutation, so the same code imports differently depending on who started it; it happens after some imports have resolved, so it cannot fix those; and it commonly puts both a package and its parent on the path, which loads one file twice under two names. An install — editable during development — makes one canonical import path that every process agrees on.
  • How do you make an interpreter import only what was installed for it?
    Start it with `-P`, or set `PYTHONSAFEPATH=1`, so the script or current directory is never prepended (3.11 and later). Add `-E` to ignore `PYTHON*` environment variables, or use `-I` for isolated mode, which implies `-E` and also drops the user site directory. The result is a path built only from the interpreter's own directories and its environment's site-packages.

saying these in an interview costs you the question

  • Says site-packages is searched before the current directory
  • Thinks sys.path is immutable or fixed at build time
  • Cannot name PYTHONPATH as a source of entries
  • Believes a virtual environment replaces the import algorithm
  • Treats .pth files as documentation rather than executable path config

context