PYTHON / MODULES AND PACKAGES
Virtual environments
Create, activate, and verify a virtual environment, and explain how sys.prefix and pyvenv.cfg decide where third-party packages land.
What you will learn
- Detect an active environment with sys.prefix != sys.base_prefix
- Create one with python -m venv .venv and run it via .venv/bin/python
- Read pyvenv.cfg to find the base interpreter and the system-site-packages flag
- Describe activation as a PATH edit, not state stored inside Python
Understanding Virtual environments
A single system Python has one site-packages directory, so two projects that need different versions of the same library cannot both be satisfied. A virtual environment solves this by being an ordinary directory that contains its own site-packages, a bin/ (Scripts/ on Windows) directory holding a python that points at the base install, and a small text file named pyvenv.cfg. Nothing is registered globally: creating an environment writes a folder, and deleting the folder removes the environment completely.
The mechanism is prefix computation. At startup CPython looks at the path of its own executable and walks upward; if it finds pyvenv.cfg next to (or one level above) that executable, it sets sys.prefix to the environment root and keeps sys.base_prefix pointing at the real installation named by the file's home key. Because the standard library is looked up under base_prefix and third-party packages under prefix, a venv shares the stdlib with the base Python but gets a private site-packages of its own. That is why an environment costs a few megabytes rather than a full Python copy, and it is why sys.prefix != sys.base_prefix is the reliable test for being inside one.
Activation is far less magical than it looks. The activate script prepends the environment's bin directory to PATH and sets VIRTUAL_ENV in that one shell, so a bare python or pip command resolves to the environment's copy; deactivate puts PATH back. You never have to activate at all — calling .venv/bin/python directly gives exactly the same interpreter, which is what cron jobs and editors should do. Treat one environment per project as the default, keep it inside the project as .venv, and rebuild rather than repair it when something goes wrong.
import os
import sys
in_venv = sys.prefix != sys.base_prefix
print("in a virtual environment:", in_venv)
print("sys.prefix == sys.base_prefix:", not in_venv)
print("pyvenv.cfg found at sys.prefix:",
os.path.isfile(os.path.join(sys.prefix, "pyvenv.cfg")))
print("environment name:", os.path.basename(sys.prefix) if in_venv else "(none)")A virtual environment is a directory whose interpreter resolves sys.prefix to that directory, so third-party packages install into its private site-packages while the standard library is still read from the base installation.
Worked examples
Build an environment and read its config
Creates a real environment with the venv module and inspects the file that makes it an environment.
import os
import tempfile
import venv
with tempfile.TemporaryDirectory() as tmp:
env_dir = os.path.join(tmp, "demo")
venv.EnvBuilder(with_pip=False).create(env_dir)
entries = set(os.listdir(env_dir))
print("pyvenv.cfg created:", "pyvenv.cfg" in entries)
print("script directory:", "Scripts" if os.name == "nt" else "bin")
cfg = {}
with open(os.path.join(env_dir, "pyvenv.cfg")) as fh:
for line in fh:
if "=" in line:
key, value = line.split("=", 1)
cfg[key.strip()] = value.strip()
print("records base interpreter dir:", "home" in cfg)
print("include-system-site-packages:", cfg["include-system-site-packages"])Example explained
Line 1venv.EnvBuilder(...).create() is the same code path the command python -m venv runs; with_pip=False just skips bootstrapping pip.
Line 2The environment root contains only pyvenv.cfg plus bin/ and lib/ (Scripts/ and Lib/ on Windows) — no copy of the standard library.
Line 3The home key stores the directory of the base interpreter, which is how the environment still finds the stdlib at import time.
Line 4include-system-site-packages = false is the reason a fresh environment cannot see libraries already installed for the system Python.
Isolated versus system-sharing environments
Shows the single flag that decides whether an environment can import the base interpreter's third-party packages.
import os
import tempfile
import venv
def flag_for(env_dir, share):
venv.EnvBuilder(with_pip=False, system_site_packages=share).create(env_dir)
with open(os.path.join(env_dir, "pyvenv.cfg")) as fh:
for line in fh:
if line.startswith("include-system-site-packages"):
return line.split("=", 1)[1].strip()
with tempfile.TemporaryDirectory() as tmp:
print("default env :", flag_for(os.path.join(tmp, "iso"), False))
print("sharing env :", flag_for(os.path.join(tmp, "shared"), True))Example explained
Line 1system_site_packages=True corresponds to the --system-site-packages option of python -m venv.
Line 2With the flag true, the base installation's site-packages is appended to sys.path, so already-installed libraries become importable.
Line 3The default is false because shared packages reintroduce exactly the version conflicts the environment was created to avoid.
Line 4The flag is plain text in pyvenv.cfg, so you can inspect (or edit) it without recreating the environment.
The environment's python answers differently
Runs the newly created interpreter as a subprocess to show that prefix detection comes from the executable's location, not from activation.
import os
import subprocess
import sys
import tempfile
import venv
probe = ("import sys; "
"print(sys.prefix != sys.base_prefix, sys.base_prefix == %r)" % sys.base_prefix)
with tempfile.TemporaryDirectory() as tmp:
env_dir = os.path.join(tmp, "demo")
venv.EnvBuilder(with_pip=False).create(env_dir)
if os.name == "nt":
py = os.path.join(env_dir, "Scripts", "python.exe")
else:
py = os.path.join(env_dir, "bin", "python")
result = subprocess.run([py, "-c", probe], capture_output=True, text=True)
print("child :", result.stdout.strip())
print("parent:", sys.prefix != sys.base_prefix)Example explained
Line 1No activate script was sourced; the environment's interpreter was invoked by its full path and still identifies itself as an environment.
Line 2The first True in the child's output is prefix != base_prefix, decided by the pyvenv.cfg sitting beside that executable.
Line 3The second True shows both processes agree on base_prefix: the standard library is shared, not duplicated.
Line 4The parent line is False because the outer process is the plain system interpreter.
Important notes
An environment is tied to the exact base interpreter it was created from; when the system moves from python3.11 to python3.12 the old home path vanishes and the environment must be recreated.
Activation changes PATH and VIRTUAL_ENV only in the current shell, so cron jobs, systemd units, and editor run configurations should point at the absolute .venv/bin/python path instead of relying on it.
Common mistakes
Installing a library in a terminal where the environment was never activated (or with sudo pip): the files land in the system interpreter's site-packages, and .venv/bin/python then raises ModuleNotFoundError.
Renaming or moving the environment directory, or the folder above it: the absolute home path in pyvenv.cfg and the absolute shebangs in bin/ no longer resolve, so .venv/bin/pip fails with a 'bad interpreter' error.
Committing .venv/ to version control: it is platform-specific binaries and symlinks that break on another operating system and bloat the repository, while the useful information is just the dependency list.
Try it yourself
Change, predict, then run
Write a function report(path) that uses venv.EnvBuilder(with_pip=False, prompt="demo") to create an environment inside tempfile.TemporaryDirectory(), then prints every key=value pair from its pyvenv.cfg sorted by key. Run it and confirm a prompt line appears alongside home and include-system-site-packages.
Open the Python workspaceCheck your understanding
You create .venv, activate it, install a library, and it imports fine. The next day you open a fresh terminal in the same folder and run python app.py, which fails with ModuleNotFoundError. What happened?
- The new shell's PATH still points at the system interpreter, whose site-packages has no copy of the library
- The installed files were temporary and were removed when the first terminal was closed
- Running a file as python app.py bypasses site-packages; only python -m imports installed libraries
- A virtual environment has to be re-created in every new terminal session before it can be used
Show answer
Activation is only a PATH and VIRTUAL_ENV edit inside one shell, so a fresh terminal resolves the bare name python to the system interpreter, which has a different site-packages. Option 2 is tempting but wrong: the library is still on disk under .venv/lib/... and .venv/bin/python imports it immediately, without any activation.