env.dev

PYTHONPATH

Extra directories Python prepends to sys.path, the module import search path. Useful for quick local hacks; as permanent configuration it causes shadowed imports and 'works on my machine' bugs.

Last updated:

PYTHONPATH is a list of directories (colon-separated on Unix, semicolon on Windows) that the interpreter inserts into sys.path ahead of installed site-packages. Every import statement then searches those directories first. That makes it a quick way to run code against a checkout without installing it — and exactly as dangerous as it sounds, because anything in a PYTHONPATH directory silently shadows an installed package with the same name. It has no effect on which python binary runs (that is PATH), and it does not install anything. Modern Python workflows mostly replace it: `pip install -e .` puts your project on sys.path properly, and pytest grew a `pythonpath` ini option in pytest 7.0 specifically so test suites could stop relying on the environment variable.

Provider
General / OS
Category
runtime
Set by
Set manually in the shell, CI config, or IDE run configurations
Example
/opt/myproject/src
Gotcha: A globally exported PYTHONPATH leaks into every Python on the machine — including ones you do not think of as yours. System tools written in Python (dnf, gcloud, aws-cli v1) and any virtualenv you activate all see it, so a stale entry pointing at an old project produces baffling ImportError and AttributeError failures in unrelated tools. If a Python tool misbehaves only on one machine, `python -c "import sys; print(sys.path)"` and an unset PYTHONPATH is the first thing to check.

How to set PYTHONPATH

one-off run against a src/ layout

PYTHONPATH=src python -m myapp.cli

bash (colon-separated on Unix)

export PYTHONPATH="/opt/myproject/src:$PYTHONPATH"

inspect the effective search path

python -c "import sys; print('\n'.join(sys.path))"

the better long-term fix

pip install -e .  # editable install instead of PYTHONPATH

Where do PYTHONPATH entries land in sys.path?

Order is the whole story. CPython builds sys.path as: the script's own directory (or the current directory for -c and the REPL), then every PYTHONPATH entry in order, then the standard library, then site-packages. Imports take the first match walking that list — so PYTHONPATH entries outrank everything you installed with pip, and anything in them shadows same-named installed packages without a warning of any kind. A directory left on PYTHONPATH with a stale copy of your package means you can pip-install fixes forever and keep importing the old code.

bash
# see the real search order, and where each module came from
python -c "import sys; print('\n'.join(sys.path))"
python -c "import requests; print(requests.__file__)"

Python 3.11 added an escape hatch aimed at exactly this class of bug: python -P (or PYTHONSAFEPATH=1) drops the implicit script-directory/cwd entry, so a file named token.py or secrets.py sitting next to your script stops shadowing the standard library.

What PYTHONPATH does not do

  • It does not choose which Python runs — that's PATH (see the PATH page). A correct PYTHONPATH with the wrong interpreter is still the wrong interpreter.
  • It does not install anything, resolve dependencies, or register entry points — modules become importable, nothing more. Console scripts defined in pyproject.toml stay missing.
  • It is not virtualenv-aware. The variable leaks into every environment you activate, which defeats the isolation virtualenvs exist to provide.

The editable install replaced it — use that

The legitimate historical use of PYTHONPATH — "run my code from a checkout without installing it" — has a proper packaging answer: pip install -e . puts your project on sys.path through the import system, visibly (it shows in pip list), per-environment, and without shadowing risk. For test suites, pytest 7.0 added the pythonpath ini option so the old "export PYTHONPATH=src before running tests" ritual can live in committed config instead of fragile shell state:

toml
# pyproject.toml
[tool.pytest.ini_options]
pythonpath = ["src"]

What survives as a good use of the variable is the one-off: PYTHONPATH=src python -m myapp.cli scoped to a single command, in a Makefile or a .env file loaded per-project. Scoped is the keyword — the failure mode is always the global export.

Why a global PYTHONPATH breaks other people's tools

Every Python process on the machine inherits it: the Python inside gcloud, aws-cli v1, dnf, Ansible, your editor's language server. A PYTHONPATH entry containing, say, an old six.py or a half-compatible yaml package gets imported by those tools in preference to their vendored versions, producing AttributeError crashes in software you never touched. This failure is common enough that Google's gcloud troubleshooting docs explicitly suggest unsetting PYTHONPATH. If a system Python tool crashes with an import-related traceback, run it with env -u PYTHONPATH before filing a bug. The broader catalog of Python's environment knobs — PYTHONHOME (far more destructive), PYTHONDONTWRITEBYTECODE, PYTHONUNBUFFERED — is in the Python environment variables guide.

Docker and CI: the acceptable exception

Inside a container image you control end to end, ENV PYTHONPATH=/app/src is defensible — there is exactly one Python, one app, and no neighboring tools to poison. Even there, copying the project in and running pip install --no-deps . in the build stage is barely more work and keeps the import mechanism standard. Where you see PYTHONPATH most in the wild is CI pipelines and Airflow/Spark deployments gluing zip artifacts onto the path; inherit those patterns knowingly rather than cargo-culting them into application code. Wiring per-environment values like this through compose files is covered in the docker-compose environment variables guide.

Frequently Asked Questions

Should I set PYTHONPATH in production?

Almost never. Install your package (pip install . or an editable install during development) so sys.path is managed by the packaging layer. PYTHONPATH in production config is invisible to dependency tooling, shadows installed packages, and breaks the moment the directory layout changes.

Why does my import still fail with PYTHONPATH set?

Check three things: the separator (colon on Unix, semicolon on Windows — the wrong one turns two paths into one bogus path), whether the directory contains the package itself rather than its parent, and whether you are running a different interpreter than the shell you exported it in (IDEs and cron do not inherit your shell profile).

Was this helpful?

Stay up to date

Get notified about new guides, tools, and cheatsheets.

Browse all 244 environment variables →