What does activating a Python virtual environment actually change in your shell?
answer
- Nothing is installed or copied at activation
- The shell changes, the interpreter does not
- One variable is prepended, one is exported
- PATH, VIRTUAL_ENV, PYTHONHOME, prompt
- .venv/bin/python works without activation
basics
~20 sActivation 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 linespython3 -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
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.
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.
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.
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