Python Virtual Environments

Python  /  Packaging  /  Developer Tooling

venv,
pip, pipx

venv: stdlib since Python 3.3  ·  pip: Python's package installer
pipx: isolated CLI tool installer  ·  uv: the fast newcomer
PEP 405 (2012)  ·  PEP 668 (2023, "externally managed")

Python has a packaging problem — or rather, it has had several, at different points in its history. The virtual environment is the solution the ecosystem settled on: a self-contained directory that gives each project its own Python interpreter, its own packages, and no ability to contaminate anything else. Understanding why it exists requires understanding what it replaced.

The problem: one Python, many projects

A Python installation is a single interpreter with a single set of installed packages living in a single directory. When you install a package with

plaintext
pip install requests
, it goes into that interpreter's
plaintext
site-packages
folder — the same place every other package for every other project goes. This is fine for one project. For two projects that need different versions of the same library it is a conflict waiting to happen. For a developer machine hosting dozens of projects across several years it is an inevitability.

The canonical failure mode: Project A was built in 2021 against

plaintext
Django==3.2
. Project B requires
plaintext
Django==4.2
. Installing one breaks the other. There is no mechanism in a shared installation to have both. The system-level Python — the interpreter shipped with a Linux distribution or macOS — adds a further complication: its packages are managed by the OS package manager, and
plaintext
pip
installing into it can break system tools that depend on specific library versions they did not expect to change.

↳ PEP 668: the externally managed environment

Since Python 3.11 and formalised in PEP 668, most Linux distributions mark their system Python as "externally managed." Attempting to

plaintext
pip install
into it produces an error rather than silently corrupting system packages. The error message points the user toward virtual environments. This is the correct behaviour — and it broke a lot of old tutorials that assumed
plaintext
pip install
always worked.

What a virtual environment actually is

A virtual environment is a directory — nothing more. It contains a copy of (or symlink to) a Python interpreter, a fresh

plaintext
site-packages
folder, and a set of activation scripts that modify the current shell session to prefer this environment's interpreter and tools over the system defaults. When activated,
plaintext
python
resolves to the venv's interpreter and
plaintext
pip
installs into the venv's
plaintext
site-packages
. When deactivated, everything reverts. The system Python is untouched throughout.

Virtual environment directory layout  ·  Linux/macOS
myproject/
├── .venv/                    # convention: name it .venv
│   ├── bin/                  # Scripts/ on Windows
│   │   ├── python            # symlink → system interpreter
│   │   ├── python3           # symlink → same
│   │   ├── pip               # pip scoped to this env
│   │   └── activate          # source this to activate
│   ├── lib/
│   │   └── python3.12/
│   │       └── site-packages/  # packages land here
│   │           ├── requests/
│   │           └── ...
│   └── pyvenv.cfg            # home = /usr/bin/python3
├── requirements.txt
└── main.py

The

plaintext
pyvenv.cfg
file is the key to how Python finds its packages. It records the path to the base interpreter —
plaintext
home = /usr/bin/python3
— and a flag,
plaintext
include-system-site-packages = false
, that controls whether the venv can see the system's packages at all. With the flag false (the default), the venv is hermetically sealed from the system installation. With it true, the venv inherits the system packages while still being able to install its own on top. The false default is almost always what you want.

Creating and using a venv

The

plaintext
venv
module is part of Python's standard library — no installation required. The entire lifecycle of a virtual environment, from creation to deactivation, uses a handful of commands that are the same on every platform, with minor shell-specific variations.

venv lifecycle  ·  Linux / macOS
# Create a virtual environment in .venv/
python3 -m venv .venv

# Activate it — modifies PATH for this shell session
source .venv/bin/activate

# Your prompt now shows (.venv) to indicate activation
(.venv) $ which python
/home/user/myproject/.venv/bin/python

# Install packages into the venv
(.venv) $ pip install requests flask

# Freeze current dependencies to a file
(.venv) $ pip freeze > requirements.txt

# Deactivate — restores the original shell environment
(.venv) $ deactivate

# Delete the venv entirely — it's just a directory
$ rm -rf .venv
venv lifecycle  ·  Windows (PowerShell)
# Create
python -m venv .venv

# Activate (PowerShell)
.venv\Scripts\Activate.ps1

# Activate (cmd.exe)
.venv\Scripts\activate.bat

# PowerShell execution policy may block scripts — fix with:
Set-ExecutionPolicy -ExecutionPolicy RemoteSigned -Scope CurrentUser

# Install, freeze, deactivate — identical to Linux
(.venv) PS> pip install requests
(.venv) PS> pip freeze > requirements.txt
(.venv) PS> deactivate

↳ Never commit .venv to version control

The

plaintext
.venv
directory contains compiled bytecode, symlinks to the system interpreter, and platform-specific binaries. It is not portable between machines and should never be committed to git. Add
plaintext
.venv/
to
plaintext
.gitignore
. What you commit instead is
plaintext
requirements.txt
— the list of packages another developer can install with
plaintext
pip install -r requirements.txt
to recreate the environment identically.

pip: the package installer

plaintext
pip
— Package Installer for Python — is the standard tool for installing packages from PyPI, the Python Package Index. When invoked inside an active virtual environment, it installs into that environment's
plaintext
site-packages
. When invoked outside any virtual environment, on a system where PEP 668 is not enforced, it installs globally. On a modern Linux distribution with PEP 668 active, global installation is blocked and pip will tell you to use a venv instead.

pip  ·  common operations
# Install a package (into active venv, or globally if none active)
pip install requests

# Install a specific version
pip install "django==4.2.7"

# Install with version constraints
pip install "flask>=2.0,<3.0"

# Install from a requirements file
pip install -r requirements.txt

# Install in editable mode (for local development)
pip install -e .

# List installed packages
pip list

# Show details about a package
pip show requests

# Uninstall a package
pip uninstall requests

# Upgrade pip itself
pip install --upgrade pip

# Install GLOBALLY — bypass venv, use with caution
# (blocked on PEP 668 systems without --break-system-packages)
pip install --break-system-packages requests  # ← usually wrong

The

plaintext
--break-system-packages
flag is worth understanding. It is not, as the name might imply, a flag that makes pip do something dangerous to your system packages. It is a declaration by the user that they understand the risk and want to proceed anyway. On systems enforcing PEP 668, it is the only way to install into the system Python without using a venv. It should be used sparingly — ideally only for bootstrapping tools that manage environments themselves, like
plaintext
pipx
.

↳ pip install globally vs. inside a venv

The same

plaintext
pip install requests
command does something entirely different depending on context. Inside an active venv, it installs into
plaintext
.venv/lib/pythonX.Y/site-packages/
and affects only that project. Outside any venv, it targets the interpreter that pip belongs to — typically the system Python or a user-level installation. The safest mental model: always activate a venv first. If you find yourself typing
plaintext
pip install
without a venv active, pause and ask whether you mean to.

pipx: pip for applications, not libraries

plaintext
pip
installs Python packages. Some of those packages are libraries you import in your code —
plaintext
requests
,
plaintext
flask
,
plaintext
numpy
. Others are command-line tools you run directly —
plaintext
black
,
plaintext
ruff
,
plaintext
httpie
,
plaintext
youtube-dl
. The distinction matters because CLI tools and libraries have different installation requirements.

A library installed into a project's venv is used by that project's code. A CLI tool, by contrast, should be available globally — you want to type

plaintext
black .
from any directory, not just from projects where you happened to install it. But installing CLI tools into the system Python with
plaintext
pip install black
creates the dependency pollution problem: different tools may conflict with each other or with system packages, and uninstalling them cleanly is often incomplete.

plaintext
pipx
solves this elegantly. It installs each command-line tool into its own dedicated virtual environment, then exposes only the tool's executable on the system PATH. The user gets a globally accessible command; the package and all its dependencies are isolated in a private venv that never interacts with any other environment. Installing ten CLI tools with pipx gives you ten isolated environments, zero conflicts, and a clean uninstall path for each.

pipx  ·  installing and managing CLI tools
# Install pipx itself (bootstrapping — one of the few valid global installs)
pip install --user pipx        # user-level, not system
pipx ensurepath                # adds ~/.local/bin to PATH

# Or via system package manager (preferred on Linux)
apt install pipx               # Debian/Ubuntu
brew install pipx              # macOS

# Install a CLI tool — gets its own isolated venv automatically
pipx install black
pipx install ruff
pipx install httpie
pipx install poetry

# The tool is now available globally
black --version
black, 24.3.0 (CPython 3.12.2)

# Upgrade a specific tool
pipx upgrade black

# Upgrade all tools at once
pipx upgrade-all

# List installed tools and their environments
pipx list

# Uninstall cleanly — removes the entire isolated venv
pipx uninstall black

# Run a tool once without installing it permanently
pipx run cowsay "hello from a temporary venv"

The rule of thumb: use pip inside a venv for libraries your code imports. Use pipx for tools you run from the command line.

The landscape: uv and what comes next

The Python packaging ecosystem has historically been fragmented —

plaintext
pip
,
plaintext
virtualenv
,
plaintext
conda
,
plaintext
poetry
,
plaintext
pdm
,
plaintext
hatch
, and others each solving overlapping subsets of the problem. The most significant recent development is
plaintext
uv
, a package manager and virtual environment tool written in Rust by Astral (the team behind
plaintext
ruff
). It is a drop-in replacement for
plaintext
pip
and
plaintext
venv
that runs ten to one hundred times faster, handles dependency resolution, and manages Python versions without requiring a separate tool like
plaintext
pyenv
.

uv  ·  the fast alternative
# Install uv
curl -LsSf https://astral.sh/uv/install.sh | sh

# Create a venv (same result as python -m venv, but faster)
uv venv .venv

# Install packages — resolves and installs in milliseconds
uv pip install requests flask

# Install from requirements.txt
uv pip install -r requirements.txt

# Full project workflow (replaces pip + venv + pyenv)
uv init myproject
uv add requests            # adds to pyproject.toml, installs
uv run python main.py      # runs in the project's managed env

# Install and manage Python versions
uv python install 3.12
uv python install 3.11
Task pip + venv pipx uv
Create venv python -m venv .venv automatic (per tool) uv venv .venv
Install library pip install X not intended for uv pip install X
Install CLI tool pip install X (messy) pipx install X ✓ uv tool install X
Global install pip install (risky) isolated globally ✓ uv tool install
Speed baseline baseline 10–100× faster
Python version mgmt no (needs pyenv) no yes, built-in
Lock file requirements.txt N/A uv.lock
Stdlib dependency yes (Python 3.3+) no (install separately) no (install separately)
The mental model

Python packaging confusion usually stems from one of three misunderstandings: not knowing whether a venv is active when running pip, not knowing which Python a command resolves to, or treating CLI tools the same as libraries. Once those three distinctions are clear, the rest follows.

The simplest defensible workflow for a new project: create a

plaintext
.venv
with
plaintext
python3 -m venv .venv
, activate it, install everything with
plaintext
pip
, and freeze with
plaintext
pip freeze > requirements.txt
. That is it. No additional tooling required. For CLI tools you use across projects — formatters, linters, HTTP clients, deployment utilities — install them with
plaintext
pipx
so they are globally available without polluting anything. For a faster, more integrated experience on new projects,
plaintext
uv
handles all of this with a single tool and a unified interface.

✓ The decision tree

Is it a library your code imports? →

plaintext
pip install
inside an active venv. Is it a CLI tool you run from the command line? →
plaintext
pipx install
. Do you want speed and an all-in-one workflow? →
plaintext
uv
for everything. Are you on a system Python that blocks pip? → Create a venv first, then pip inside it — or use pipx for tools, uv for projects.

The virtual environment is not a workaround for a broken packaging system. It is the correct abstraction for the problem — each project gets its own isolated dependency graph, and the system stays clean. The tools that manage it have proliferated because Python is used for everything from embedded scripts to enterprise web services, and different contexts have different needs. The venv itself, however, is always the same: a directory, a symlink, a modified PATH, and the simple guarantee that what you install here stays here.