How do distributions installed with pip end up importable without you editing sys.path?
answer
- Something runs before your first line
- A startup module edits the search list
- Directories derived from the interpreter's prefix
- Appended, not prepended
- The site module and sys.prefix
basics
~20 sBefore your first line runs, CPython imports the site module. It derives the interpreter's site-packages directories from sys.prefix and appends them to sys.path, so anything an installer wrote there is importable with no manual path work.
solid answer
~40 sInterpreter startup imports the standard-library `site` module automatically. `site` computes the installation's library directories from `sys.prefix` and `sys.exec_prefix` — on Unix that is `<prefix>/lib/pythonX.Y/site-packages`, on Windows `<prefix>\Lib\site-packages` — and appends each existing one to `sys.path`. Optionally it also appends a per-user directory (`site.USER_SITE`) when `site.ENABLE_USER_SITE` is true. An installer does not edit `sys.path` or `PYTHONPATH`; it just unpacks files into one of those directories, and the next interpreter start finds them. You can see exactly what was decided with `python -m site`, which prints `sys.path`, `USER_BASE`, `USER_SITE` and `ENABLE_USER_SITE`. Site directories land near the end of `sys.path`, after the script's own directory and after `PYTHONPATH`, which is why a local file can shadow an installed distribution.
code
python · 6 linesimport site
import sys
print(sys.prefix)
print(site.getsitepackages())
print(site.ENABLE_USER_SITE, site.getusersitepackages())go deeper
Be ready to say what makes an installed distribution importable: a startup module puts site-packages on the search list. Know that a local file named after a module shadows the installed one.
Explain the mechanics: the interpreter computes its prefixes, builds the platform's site-packages layout from them, and appends the directories. Be able to read python -m site output and say which entry a module came from.
Show how you diagnose the 'installed but not importable' report in a real deployment — compare the two interpreters' prefixes and assembled paths rather than guessing, and prove which file was imported with __file__.
Own the policy angle: pinning one interpreter per service, forbidding ambient path edits, and making the environment reproducible so the assembled search list is identical in CI and in production.
### Two different things called "a package" Python's vocabulary collides here, so fix the terms first. An **installed distribution** is the thing an installer downloads and unpacks — a project with a name on an index. An **importable package** is a directory of `.py` files with an `__init__.py` (or a namespace package) that `import` can find. Installing a distribution puts one or more importable modules and packages into a directory; making that directory visible to `import` is a separate job, and that job belongs to the `site` module. ### What startup actually does When CPython boots, it works out where it is installed and sets `sys.prefix` and `sys.exec_prefix` (the pure-Python and platform-specific roots). It builds an initial `sys.path` from a fixed recipe: the script's directory (or the current directory in the REPL), then the entries of the `PYTHONPATH` environment variable, then the standard library directories. Then — as the last step before your code runs — it imports `site`. `site` is an ordinary standard-library module with a side effect at import time. It takes the prefixes it was given and, for each one, constructs the platform's library layout: on Unix `<prefix>/lib/pythonX.Y/site-packages`, on Windows `<prefix>\Lib\site-packages`. Each such directory that exists is **appended** to `sys.path`, and each is then scanned for `.pth` files, whose lines can add still more directories. `site` also computes a per-user directory, exposed as `site.USER_SITE` under `site.USER_BASE`, and appends it when `site.ENABLE_USER_SITE` is true. Two helper functions report the results after the fact: `site.getsitepackages()` and `site.getusersitepackages()`. None of this is hard-coded into the interpreter binary as a literal path. It is computed at every start from where the interpreter is, which is exactly why moving or copying an installation, or running a different interpreter binary, changes what is importable without any file inside your project changing. ### Order matters more than most people expect Site directories are *appended*, so they sit near the end of the search list. The order that results is roughly: 1. the script's own directory (or `''` for the REPL), 2. entries from `PYTHONPATH`, 3. the standard library, 4. site-packages directories added by `site`, 5. anything a `.pth` file appended. The practical consequence is the classic beginner bug: a file named `random.py`, `email.py` or `types.py` next to your script wins over the standard library, and a file named after an installed distribution's module wins over the installed copy. The error looks like the installed code is broken; the cause is search order. `python -c "import x; print(x.__file__)"` settles it in one line. ### Why an installer does not need to touch anything Because `site` derives the directories from the running interpreter, an installer's whole job is "unpack into the right directory for *this* interpreter". That is why `python -m pip install ...` is the safe invocation: it binds the install to the interpreter you named, rather than to whichever `pip` executable happens to be first on `PATH`. When people say a distribution "installed but won't import", the answer is almost always that it landed in a different interpreter's site-packages — two different `sys.prefix` values, two different search lists. ### Seeing it `python -m site` prints the assembled `sys.path`, the user base and user site directories (annotating whether they exist), and the `ENABLE_USER_SITE` flag. `site.getsitepackages()` gives just the site directories for the running interpreter. Comparing that output between an environment that works and one that does not is the fastest way to explain a "module not found" report, and it is a completely mechanical comparison: same interpreter, same prefixes, same list. This behaviour is stable across every currently supported release, including 3.14. What varies between platforms is only the directory *layout* under the prefix, not the mechanism. ### The per-user directory, and when it is missing Besides the interpreter's own site-packages, `site` can add a per-user directory — `site.USER_SITE`, under `site.USER_BASE` — which is where a user-level install lands when you do not have write access to the interpreter's own tree. It is added only when `site.ENABLE_USER_SITE` is true, and it is one of the few places where two machines running the same interpreter version legitimately end up with different search lists: the directory exists on one and not the other. `python -m site` annotates both with `(doesn't exist)` when they are absent, which makes the check a one-liner rather than an argument. Finally, remember that all of this is *optional* machinery layered on top of import, not part of it. Startup can be told to skip `site` entirely; when it is skipped, the standard library still imports fine and every installed distribution disappears, because nothing appended those directories. That is the cleanest demonstration that `import` never searches the filesystem at large — it only ever walks the list that startup handed it.
- Where does the site module get the prefix it builds those directories from?From `sys.prefix` and `sys.exec_prefix`, which the interpreter computes at startup from the location of the executable (and, inside a virtual environment, from the `pyvenv.cfg` beside it). `site` then joins the platform's library layout onto them, which is why the resulting path differs between Unix and Windows and why moving an installation changes what is importable.
- What sits on sys.path ahead of the site-packages directories?The script's own directory (or `''` in the REPL), then any `PYTHONPATH` entries, then the standard library. Because `site` appends, anything earlier shadows an installed distribution — a local `email.py` or a file named after a distribution's module wins. Printing `module.__file__` after importing tells you which copy you actually got.
- Why is `python -m pip install` preferred over calling pip directly?`python -m pip` installs into the site-packages of the interpreter you just named, so the files land where that interpreter's `site` module will look. A bare `pip` resolves through `PATH` and may belong to a different installation, which produces the classic 'it installed but it won't import' report — two interpreters, two prefixes, two search lists.
The interpreter is handed an empty address book at boot; the site module fills in the standard addresses for wherever this particular interpreter lives, then import reads the book top to bottom.
saying these in an interview costs you the question
- Claims the installer edits sys.path or PYTHONPATH
- Thinks site-packages paths are hard-coded in the binary
- Believes import scans the whole filesystem for a name
- Confuses PYTHONPATH entries with site-packages directories
- Says site-packages is searched before the script's directory