skip to content

Interpreter Isolation with venv

What a venv actually is — its own site-packages plus a pyvenv.cfg pointing at a base interpreter — and why activation is only PATH manipulation. Saying that .venv/bin/python needs no activation is the answer.

part ofPythonoverview, primer and where to startread it →
on this pageshow

questions

4

What does activating a Python virtual environment actually change in your shell?

level: juniorimportance: must knowfreq 75%

answer

  1. Nothing is installed or copied at activation
  2. The shell changes, the interpreter does not
  3. One variable is prepended, one is exported
  4. PATH, VIRTUAL_ENV, PYTHONHOME, prompt
  5. .venv/bin/python works without activation

basics

~20 s

Activation is shell bookkeeping only. Sourcing the activate script prepends the environment's bin directory to PATH, exports VIRTUAL_ENV, unsets PYTHONHOME and marks the prompt. Calling .venv/bin/python directly uses the same environment with no activation at all.

solid answer

~40 s

`python -m venv .venv` writes a small directory tree: entry points in `.venv/bin` (the `Scripts` directory on Windows), a private `site-packages`, and a `pyvenv.cfg` recording the base installation. Activation does none of that work. `source .venv/bin/activate` prepends `.venv/bin` to `PATH`, exports `VIRTUAL_ENV`, unsets `PYTHONHOME` if it was set, decorates the prompt, and defines a shell function `deactivate` that restores the saved values. Nothing about the interpreter changes: the environment is decided by *which interpreter binary you execute*, and `.venv/bin/python` finds its own `pyvenv.cfg` from its own location. That is why non-interactive contexts — CI steps, service unit files, cron entries, Makefile recipes, each of which gets its own shell — should invoke `.venv/bin/python -m ...` by absolute path rather than trying to source an activate script.

code

console · 6 lines
console
python3 -m venv .venv
source .venv/bin/activate
command -v python
echo "$VIRTUAL_ENV"
deactivate
.venv/bin/python -c "import sys; print(sys.prefix)"

go deeper

for a junior

Be ready to say plainly that activation edits PATH and the prompt, and that running .venv/bin/python directly works without it. Knowing the environment lives in a project directory you can delete and recreate is the core of it.

for a middle

Explain the mechanics: which variables the script saves and restores, why it must be sourced rather than executed, and why the interpreter you launch — not any variable — is what selects the environment.

for a senior

Show where this bites in production: per-step shells in CI, service managers and cron that never run a shell, Makefile recipes, and scrubbed subprocess environments. Reach for absolute interpreter paths and be able to justify that as the robust default.

for a principal

Own the convention across a fleet: one documented rule for how services, CI and developer tooling address an interpreter, so no runbook depends on an interactive shell state that automation cannot reproduce.

### What `python -m venv` actually builds `python -m venv .venv` asks the *currently running* interpreter to build a small directory tree. On POSIX you get `.venv/bin` containing `python`, `python3` and a version-suffixed name such as `python3.14` — symlinks to (or, with `--copies`, copies of) the interpreter that created the environment — plus the activation scripts and a `pip` entry point. On Windows the same content lives in the `Scripts` directory. Beside it sit `.venv/lib/python3.14/site-packages`, which starts almost empty, and `.venv/pyvenv.cfg`, a short text file recording the base installation. That is the entire environment: somewhere to put packages, an entry point, and a note saying which real Python it borrows a standard library from. ### Activation edits the shell, and only the shell Read the activate script and there is no magic in it. It saves the current `PATH` into a private variable and prepends the environment's `bin` directory; it exports `VIRTUAL_ENV` pointing at the environment root and, on current CPython, `VIRTUAL_ENV_PROMPT`; it unsets `PYTHONHOME` if it was set, because a stale `PYTHONHOME` would send the interpreter looking for its standard library somewhere else; it prefixes the prompt string; and it defines a shell *function* called `deactivate` that puts every one of those saved values back. It does **not** set `PYTHONPATH`. It does **not** modify `sys.path`. It does not copy or install anything, and it cannot reach into a process that is already running. So the single thing activation buys you is name lookup: typing `python`, `pip` or an installed console script finds the environment's copies first, because that directory is now at the front of `PATH`. Real convenience — but convenience, not mechanism. ### Why calling the interpreter directly is equivalent When any CPython process starts it resolves the path of its own executable, walks up one directory, and looks for `pyvenv.cfg`. If it finds one, it treats the directory containing the executable's parent as `sys.prefix` and records the original installation in `sys.base_prefix`; the environment's `site-packages` then lands on `sys.path` and the standard library continues to be loaded from the base installation. That lookup depends on nothing but the path of the binary you ran. `.venv/bin/python -c "import sys; print(sys.prefix)"` therefore prints the environment whether or not any shell variable was ever touched, and `.venv/bin/python -m pip install ...` installs into that environment for the same reason. ### Where the distinction stops being pedantry * **CI pipelines.** Most runners execute each step in a fresh shell, so `source .venv/bin/activate` in step one is simply gone by step two. Calling the absolute interpreter path in every step always works. * **Service managers, cron and supervisors.** These launch a process directly, often with no shell at all, or with a minimal `/bin/sh` that has no `source` builtin. The correct configuration names `/srv/app/.venv/bin/python` as the executable; there is nothing to activate. * **Makefile recipes.** Each recipe line runs in its own shell, so activation on one line has evaporated by the next. * **Editors and debuggers.** They ask you for an interpreter *path*, not for an activation step, because the path is what actually selects the environment. * **Subprocesses.** A child launched with a scrubbed or replaced environment loses the `PATH` edit, so a bare `python` inside it resolves to whatever the system offers. ### The failures people actually hit Using `source` in a POSIX `sh` script that does not have it (use `.` instead); PowerShell refusing to run the activation script under a restrictive execution policy; stacking activations of two different environments and expecting one `deactivate` to unwind both; deleting the environment directory while it is still on `PATH`, so `python` resolves to a path that no longer exists; and assuming activation gives isolation it never promised — a globally exported `PYTHONPATH` still injects its directories, activated or not. ### The sentence to say in an interview Activation is a `PATH` edit with a prompt change attached; the interpreter you execute is what decides the environment. Everything else follows from that. ### What reads VIRTUAL_ENV, if not the interpreter The variable is not useless — it is a *signal for tooling*. Prompt themes read it to show which environment you are in, some installers refuse to touch a global installation while it is set, and editors and shell helpers use it to guess a default interpreter. None of that is the interpreter's own behaviour, and none of it changes how imports resolve. Keep the two apart in your head: `VIRTUAL_ENV` is documentation for humans and tools, while `pyvenv.cfg` beside the executable is what the interpreter actually consults. ### Windows, briefly The same environment carries three activation scripts in its `Scripts` directory: one for a POSIX-style shell, a `.bat` file for the classic command prompt, and an `Activate.ps1` for PowerShell, which a restrictive execution policy can refuse to run. All three do the same job — reorder `PATH`, export `VIRTUAL_ENV`, decorate the prompt — and all three are equally optional, because invoking the environment's `python.exe` by path selects the environment exactly as it does on POSIX.

  • A CI job activates the environment in one step and the next step cannot import the packages. What happened?
    Most runners execute each step in a separate shell process, and a `PATH` edit dies with the shell that made it. The activation in step one never reaches step two. Either do the work in the same step, or — far better — stop activating and call the interpreter by absolute path, `.venv/bin/python -m ...`, in every step that needs it. The same reasoning applies to Makefile recipes, where each line is its own shell.
  • Does activation change what an already-running Python process can import?
    No. The activate script edits shell variables, and `PATH` is consulted only when a program name is resolved for a newly launched process. A running interpreter has already computed `sys.path` at startup from its own executable location; nothing an outside shell exports afterwards will change it. If you need a running process to see different packages you have to restart it under the other interpreter.
  • Why is `deactivate` not a program on disk?
    It has to modify the *current* shell's variables, and a child process cannot do that to its parent. So the activate script, which you `source` into the current shell rather than execute, defines `deactivate` as a shell function that restores the saved `PATH`, prompt and `PYTHONHOME` and unsets `VIRTUAL_ENV`. That is also why activation must be sourced: running the script as a program would edit a shell that exits immediately.

Activation is like putting a toolbox at the front of the shelf so your hand reaches it first; the tools work exactly the same if you walk over and pick one up by name.

saying these in an interview costs you the question

  • Says activation installs or copies Python into the project
  • Believes imports fail unless the environment is activated first
  • Thinks activation sets PYTHONPATH to the environment's site-packages
  • Sources the activate script in a Makefile recipe and expects it to persist
  • Cannot say what deactivate restores or why it is a shell function
  • Claims a service unit file needs to activate before starting the app

context

open as a page

How does .venv/bin/python know it is running inside a virtual environment?

level: middleimportance: should knowfreq 55%

basics

~20 s

At startup the interpreter resolves the path of its own executable and looks for a pyvenv.cfg file beside its directory. Finding one, it sets sys.prefix to the environment, records the original installation in sys.base_prefix, and uses the environment's site-packages.

open as a page

An invoice-PDF renderer's .venv was copied from a laptop into the release bundle and the service dies at startup. Why does copying a venv break it, and what should ship instead?

level: seniorimportance: should knowfreq 45%

basics

~20 s

A virtual environment is full of absolute paths pinned to the machine that built it: the home key in pyvenv.cfg, console-script shebangs, and the interpreter symlink. Ship pinned requirements and build the environment on the target instead.

open as a page

When is `python -m venv --system-site-packages` the right call, and what does it cost?

level: middleimportance: nice to knowfreq 18%

basics

~20 s

It writes include-system-site-packages = true into pyvenv.cfg, appending the base installation's site-packages to sys.path after the environment's own. Use it to reuse a binary package the base interpreter already has; it costs you reproducibility and clean isolation.

open as a page