skip to content

Can PYTHONPATH shadow a standard-library module, and does python -P stop it?

level: middleimportance: should knowfreq 38%

answer

  1. Order in the list decides everything
  2. Above the standard library, below the first entry
  3. One environment variable is enough
  4. -P drops only the implicit entry
  5. Isolated startup is what ignores PYTHONPATH

basics

~20 s

Yes to the first, no to the second. PYTHONPATH entries are inserted after the implicit first entry but ahead of the standard library, so a matching .py there wins. -P suppresses only the implicit entry and leaves PYTHONPATH in force.

solid answer

~40 s

`sys.path` is assembled in a fixed order: the implicit first entry (script directory, working directory, or `''`), then every `PYTHONPATH` directory in the order given, then the standard library, then `site-packages`. Because `PYTHONPATH` sits ahead of the standard library, a `random.py` in a `PYTHONPATH` directory is the `random` module for that whole process — a hijack that needs only control of one environment variable in the launch environment, since a module body executes on import. `-P` and `PYTHONSAFEPATH` are widely misread as "safe imports": they set `sys.flags.safe_path` and drop only the implicit first entry. Ignoring `PYTHONPATH` requires isolated startup, or better, a supervisor that scrubs the variable out of the environment it hands to the child.

code

console · 5 lines
console
python3 -c 'import sys; print(sys.path[:3])'
PYTHONPATH=/tmp/inject python3 -c 'import sys; print(sys.path[:3])'
PYTHONPATH=/tmp/inject python3 -P -c 'import sys; print(sys.flags.safe_path, sys.path[0])'
PYTHONPATH=: python3 -c 'import sys; print(sys.path[:2])'
PYTHONSAFEPATH=0 python3 -c 'import sys; print(sys.flags.safe_path)'

go deeper

for a junior

Remember that PYTHONPATH adds directories that Python searches before the standard library, so a stray file on it can silently replace a standard module. Do not set it casually in shell profiles.

for a middle

Be able to recite the assembly order — implicit first entry, PYTHONPATH, standard library, site-packages — and state clearly that -P removes only the first of those, leaving PYTHONPATH fully effective.

for a senior

Show that you treat the launch environment as untrusted input to the import system: construct the child environment explicitly, prefer isolated startup where inheritance is uncertain, and log sys.path at boot so an incident can be answered with evidence.

for a principal

Own where the environment is fixed across the fleet — supervisor, unit file, image, or job spec — and make an inherited PYTHONPATH a deliberate, reviewed exception rather than something any launcher can introduce unnoticed.

### The assembled order `sys.path` is built at startup in a fixed order, and the order is the entire answer: 1. the implicit first entry — the script's directory, the working directory for `-m`, or `''` for `-c`, stdin and the REPL (absent when safe path mode is on); 2. every directory listed in `PYTHONPATH`, in the order given; 3. the standard-library zip and the standard-library directory; 4. `site-packages` and the user site directory. `PYTHONPATH` therefore sits **above the standard library**. A `random.py` in a `PYTHONPATH` directory is the `random` module for that process, exactly as a file beside the script would be — with the important difference that it needs no writable working directory and no filesystem access at all near the application. Control of one environment variable in the process's launch environment is enough to choose what the interpreter imports, and a module body executes on import. ```console python3 -c 'import sys; print(sys.path[:3])' PYTHONPATH=/tmp/inject python3 -c 'import sys; print(sys.path[:3])' ``` The second command shows `/tmp/inject` inserted between the empty first entry and the standard library. ### Two sharp edges in the variable itself * **An empty component means the working directory.** `PYTHONPATH=:` — or any value with a leading, trailing or doubled separator — contributes the current working directory as a path entry, the same way an empty component in the operating system's `PATH` does. A shell fragment like `PYTHONPATH="$EXTRA:$PYTHONPATH"` with `EXTRA` unset quietly hands the process its own working directory. This is a common way the variable becomes a hijack vector by accident. * **A completely empty value adds nothing.** `PYTHONPATH=` behaves like an unset variable, which is how you scrub it. ### What `-P` does and does not do `python -P`, and `PYTHONSAFEPATH` set to any non-empty value, suppress **only** the implicit first entry. `sys.flags.safe_path` becomes `True`, `sys.path[0]` becomes the standard-library zip — and `PYTHONPATH` remains fully in force, still ahead of the standard library: ```console PYTHONPATH=/tmp/inject python3 -P -c 'import sys; print(sys.flags.safe_path, sys.path[0])' ``` That prints `True` followed by `/tmp/inject`. Reading `-P` as "safe imports" is the single most common misconception about this flag, and it matters because a team that adds `-P` to a service and believes the import surface is closed has closed one of two doors. A second trap on the environment variable: `PYTHONSAFEPATH=0` **enables** safe path mode, because CPython checks these variables for non-emptiness rather than parsing them as booleans. Use `PYTHONSAFEPATH=` to leave it off. Ignoring `PYTHONPATH` requires isolated startup — the mode that ignores the `PYTHON*` environment variables and skips the user site directory — or, more robustly, launching the process from a supervisor that removes the variable from the environment it hands over rather than trusting the interpreter to ignore it. ### Why this is a real attack path, not a curiosity `PYTHONPATH` is inherited like any other environment variable, so it flows from whoever launched the process: a shell profile, a CI job definition, a unit file, a container image's environment block, a job spec. Any of those is a place an attacker who has achieved a smaller foothold can leave a persistent, quiet hook that runs code in every Python process started afterwards. It is popular precisely because it survives redeploys of the application code and leaves no trace in the application's own repository. Two consequences for how you operate services: * **Pin the environment at the launch boundary.** A service's start-up should construct the child's environment explicitly rather than passing through whatever it inherited, and `PYTHONPATH` should be absent unless the service genuinely needs it. If it needs it, the entries should be absolute and owned by the deploying identity. * **Log what you actually got.** Recording `sys.path` and `sys.flags.safe_path` at startup costs one line and is the only artefact that can answer "what did this process search?" after an incident. Configuration files claim what should have happened; the log records what did. ### The compact answer Yes, `PYTHONPATH` shadows the standard library, because its entries are inserted above the standard library and below the implicit first entry. No, `-P` does not stop it: `-P` and `PYTHONSAFEPATH` remove only the implicit first entry and set `sys.flags.safe_path`; ignoring `PYTHONPATH` needs isolated startup or a scrubbed environment.

  • What exactly does sys.flags.safe_path tell you, and what does it not?
    It is True when the interpreter started with `-P` or with `PYTHONSAFEPATH` set to a non-empty value, meaning no script directory, working directory or empty entry was prepended. It says nothing about `PYTHONPATH`, `site-packages`, or anything else on the list. If you want to know what the process will actually search, log `sys.path` itself; the flag answers one narrow question.
  • Is there a way PYTHONPATH adds the working directory without anyone intending it?
    Yes. An empty component — a leading, trailing or doubled separator, as in `PYTHONPATH=:` — contributes the current working directory, the same way an empty component in the operating system's PATH does. A shell fragment like `PYTHONPATH="$EXTRA:$PYTHONPATH"` with `EXTRA` unset produces exactly that. A wholly empty value, `PYTHONPATH=`, is treated as unset and adds nothing, which is how you scrub it.
  • If a process must inherit an untrusted environment, how do you protect its imports?
    Do not rely on flags inside the process. Remove `PYTHONPATH` and the other `PYTHON*` variables before the interpreter starts, or use isolated startup, which ignores them and skips the user site directory. Then assert at boot: log `sys.path` and `sys.flags.safe_path`, and fail fast if an unexpected entry is present. Configuration describes intent; the startup log is the only record of what actually happened.

saying these in an interview costs you the question

  • Believes PYTHONPATH is appended after the standard library
  • Says -P makes all of a process's imports safe
  • Confuses PYTHONPATH with the operating system's PATH
  • Thinks only site-packages can shadow the standard library
  • Assumes a hijack needs a writable directory near the app
  • Reads PYTHONSAFEPATH=0 as switching safe path mode off

context