Virtual Environments

A virtual environment is an isolated Python installation-within-an-installation that keeps one project's packages from colliding with another's.

A virtual environment is a self-contained directory tree that holds its own Python interpreter (or a link to one) and its own site-packages folder for installed libraries. It lets each project depend on exactly the package versions it needs without fighting other projects — or the operating system's own Python — for the same global install location.

What Is a Virtual Environment

Every Python interpreter has one site-packages directory where pip install puts third-party packages by default. If you install everything into that single global location, two projects that need different versions of the same library (say, requests==2.28 and requests==2.31) cannot both be satisfied at once. A virtual environment solves this by giving each project its own private site-packages, separate from the system interpreter's and from every other environment's.

The standard library module venv creates these environments. It ships with Python 3.3+, so no extra install is needed for the basic workflow.

Why Isolation Matters

Without isolation, installing a package for one project can silently upgrade or downgrade a package another project relies on, breaking it. Isolation also protects the operating system's own Python installation (many Linux distributions use system Python for OS tooling) from being polluted or broken by application dependencies — some distributions now refuse a bare pip install outside a virtual environment for exactly this reason.

Isolation applies to installed packages, not to the language itself: your code still runs on whichever interpreter the environment was built from.

Creating a Virtual Environment

Run the venv module against the interpreter you want the environment to use. The directory name (commonly .venv or venv) is created new and becomes the environment's home.

This creates .venv/ containing a copy or symlink of the interpreter, a fresh empty site-packages, and activation scripts.

The created directory contains, on POSIX systems, bin/ (interpreter and activation scripts), lib/pythonX.Y/site-packages/ (installed packages), and pyvenv.cfg (metadata pointing back at the base interpreter). On Windows the layout uses Scripts\ instead of bin/.

python -m venv .venv

# Explicitly choose an interpreter version if several are installed:
python3.12 -m venv .venv

Activating and Deactivating

Activation modifies the current shell session so that python and pip resolve to the environment's copies instead of the system ones. It is a per-shell, temporary change — it does not alter the filesystem or any other terminal.

A common tell that activation worked is the shell prompt gaining a (.venv) prefix, and which python (POSIX) or where python (Windows) pointing inside the environment directory.

# macOS / Linux (bash, zsh)
source .venv/bin/activate

# Windows PowerShell
.venv\Scripts\Activate.ps1

# Windows cmd.exe
.venv\Scripts\activate.bat

# Leaving the environment (any platform)
deactivate

How Activation Works Under the Hood

Activation is nothing magical — it prepends the environment's bin (or Scripts) directory to the PATH environment variable, so the shell finds the environment's python and pip executables before the system ones, and it sets VIRTUAL_ENV to the environment's path. deactivate is a shell function the activation script defines that restores the original PATH.

Because it is PATH-based, you never strictly need to activate an environment: calling its interpreter by full path works identically and is exactly what CI systems, scripts, and IDEs typically do.

# Equivalent to activating, for a single command:
.venv/bin/python -m pip install requests
.venv/bin/python my_script.py

Interpreter Selection

A virtual environment is always built from a specific base interpreter, recorded in pyvenv.cfg. Choosing which python you run -m venv with determines the Python version and build the environment will use for its entire lifetime — you cannot change a venv's Python version in place; you recreate it with a different interpreter instead.

On systems with multiple Python versions installed, use a version-qualified command (python3.11, python3.12) or, on Windows, the py launcher (py -3.12 -m venv .venv) to pick the exact interpreter deliberately rather than relying on whichever python happens to be first on PATH.

Installing Packages Into an Environment

Once activated (or invoked by full path), pip installs into the environment's own site-packages, entirely separate from the system installation.

source .venv/bin/activate
pip install requests
pip list          # only shows packages installed in THIS environment

Reproducibility

A virtual environment by itself is not portable — it is a local directory tied to a specific machine's filesystem paths and should never be committed to version control or copied between machines. What travels is a description of its contents, most simply a requirements.txt produced by pip freeze, so that anyone can recreate an equivalent (not identical, byte-for-byte) environment elsewhere.

requirements.txt pins what pip freeze saw installed; true reproducible builds with resolved, hashed dependency graphs are covered under Dependency Management.

# On the machine that has the working environment:
pip freeze > requirements.txt

# On a new machine or CI runner:
python -m venv .venv
source .venv/bin/activate
pip install -r requirements.txt

Practical Workflow

The typical per-project pattern: create one virtual environment per project (not per machine), activate it whenever you work on that project, install only what that project needs, and keep .venv/ out of version control via .gitignore — commit requirements.txt or pyproject.toml instead.

myproject/
├── .venv/            # ignored by git
├── .gitignore        # contains ".venv/"
├── requirements.txt
└── src/

Edge Cases

A few situations worth knowing about:

System site-packages leakage: by default venv isolates completely, but python -m venv --system-site-packages .venv lets the environment also see packages installed globally — useful rarely (e.g. reusing a heavy pre-built package), but it undermines isolation and is easy to forget you enabled.

Nesting: activating a second virtual environment while one is already active does not stack; it simply replaces the active one on PATH. Deactivate before switching, or just activate the new one directly.

Moving or renaming: because activation scripts and some executables embed absolute paths, a venv directory generally cannot be moved or copied to another path and keep working reliably — recreate it instead.

Best Practices

One virtual environment per project, not one shared environment for everything. Name it consistently (.venv is a common convention many tools auto-detect). Never commit the environment directory itself to version control. Always pin what you install via a requirements file or pyproject.toml so the environment is reconstructible. Recreate rather than repair a broken environment — it's cheap and disposable by design.

Complete Example

Every install after activation lands in weatherapp/.venv, leaving the system Python and every other project's environment untouched.

# Start a new project with an isolated environment
mkdir weatherapp && cd weatherapp
python3.12 -m venv .venv
source .venv/bin/activate

python -m pip install --upgrade pip
pip install requests

python - <<'PY'
import requests
print(requests.__version__)
PY

pip freeze > requirements.txt
deactivate

Mental Model and Quick Reference

Think of a virtual environment as a labeled toolbox that sits next to your project: the system Python is the workshop's shared toolbox everyone can reach for, while .venv/ is a toolbox you built just for this one job, stocked with exactly the tools (package versions) that job needs.

Create:      python -m venv .venv
Activate:    source .venv/bin/activate      (Windows: .venv\Scripts\Activate.ps1)
Deactivate:  deactivate
Which python: which python   /   where python
Freeze:      pip freeze > requirements.txt
Restore:     pip install -r requirements.txt

Key Takeaways

A virtual environment gives a project its own interpreter reference and its own site-packages, isolated from the system Python and from other projects.

venv is a standard-library module — no extra installation is required to create environments.

Activation is just a PATH change; calling the environment's python/pip by full path works identically without activating.

The environment directory is local and disposable — never commit it, and recreate it when in doubt.

Reproducibility comes from a requirements file or pyproject.toml, not from the environment directory itself.

pip install requests # run with no venv active, system-wide python -m venv.venv source.venv/bin/activate pip install requests

Installing without an active virtual environment pollutes (or is blocked by) the system Python and affects every project that shares it.

A venv directory is machine-specific and large; commit a requirements file or pyproject.toml instead, and regenerate the environment on any machine.

python -m venv .venv source .venv/bin/activate python script.py # now guaranteed to be .venv's interpreter

Without activation (or an explicit full path), python still resolves via PATH to whatever came first — which may not be the environment you just created.

What problem does a virtual environment solve that installing packages globally does not?

It gives each project its own site-packages, so different projects can depend on different, even conflicting, versions of the same package without interfering with each other or the system Python.

What actually happens when you 'activate' a virtual environment?

The activation script prepends the environment's bin/Scripts directory to PATH and sets VIRTUAL_ENV, so python/pip resolve to the environment's copies. No files are modified.

Why is it safe to delete and recreate a virtual environment, but not safe to move it to a new path?

The environment is disposable and reconstructible from a requirements file, but some of its scripts/executables embed absolute paths, so moving it can break those references; recreating from scratch avoids that entirely.

How would you create a venv using a specific Python version when multiple are installed?

Invoke that version explicitly, e.g. python3.12 -m venv .venv, or on Windows use the launcher: py -3.12 -m venv .venv.

Why should .venv/ be excluded from version control?

It's large, machine- and path-specific, and fully reconstructible from a requirements file or pyproject.toml — committing it adds bloat and fragility with no benefit.

What does --system-site-packages do, and why is it used sparingly?

It lets the virtual environment also see packages installed in the base interpreter's global site-packages, which weakens isolation — the whole reason the environment exists — so it's reserved for rare cases like reusing an expensive pre-built package.

Write the shell commands to create a virtual environment named .venv using Python 3.12, activate it, install httpx, and save the installed packages to requirements.txt.

python3.12 -m venv.venv source.venv/bin/activate pip install httpx pip freeze > requirements.txt python3.12 -m venv.venv creates the environment; source.venv/bin/activate puts its bin/ first on PATH; pip install httpx installs into the environment's site-packages; pip freeze > requirements.txt records the exact installed versions for later reproduction.

Without activating .venv, does this subprocess call use the virtual environment's interpreter? What does it print relative to the system Python's prefix?

Yes — calling the environment's interpreter by its full path uses that environment regardless of activation. sys.prefix will point inside .venv/, not the system Python's installation directory, because activation only affects PATH lookup, not which interpreter a full path invokes.

Write a short explanation (as a comment) of why pip install pandas inside one project's activated venv does not affect a second project's venv that also has pandas installed at a different version.

Isolation is directory-based: each venv's site-packages is a distinct filesystem location, so installs in one never write into another.

# .venv already created; NOT activated
import subprocess
subprocess.run([".venv/bin/python", "-c", "import sys; print(sys.prefix)"])