cheat sheet

Virtual Environments

Create, activate, and manage Python virtual environments with venv. Covers activation for every shell, pip usage, requirements files, and cleanup.

Virtual Environments — venv

What it is

venv is Python's built-in module for creating isolated virtual environments (Python 3.3+, no install required). Each environment gets its own site-packages directory so dependencies installed for one project cannot conflict with those of another or corrupt the system Python. It is the baseline tool for Python environment isolation — lightweight and always available — before reaching for higher-level managers like uv or poetry.

A virtual environment gives each project its own isolated Python installation. Never install packages into the system Python.

Create a virtual environment

bash
python -m venv .venv

Output:

text
(no output — the .venv/ directory is created)

The conventional name is .venv or venv. Use .venv so editors (VS Code, PyCharm) auto-detect it.

Activate

Activation makes .venv/bin/python and .venv/bin/pip the active interpreter.

bash
# bash / zsh
source .venv/bin/activate

# fish
source .venv/bin/activate.fish

# Windows PowerShell
.venv\Scripts\Activate.ps1

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

Output: (none — exits 0 on success)

After activation your shell prompt changes to show the environment name:

Output:

text
(.venv) user@host:~/myproject $

Check you're in the right environment:

bash
which python

Output:

text
/home/user/myproject/.venv/bin/python

Install packages

bash
pip install requests pandas
pip install "fastapi>=0.111" "pydantic>=2"   # version constraints

Output:

text
Collecting requests
  Downloading requests-2.32.3-py3-none-any.whl (64 kB)
Collecting pandas
  Downloading pandas-2.2.2-cp312-cp312-manylinux_2_17_x86_64.whl (13.0 MB)
Successfully installed certifi-2024.2.2 requests-2.32.3 pandas-2.2.2 ...

Freeze / requirements

bash
# Save exact versions of all installed packages
pip freeze > requirements.txt

# Install from requirements
pip install -r requirements.txt

Output of pip freeze:

text
certifi==2024.2.2
charset-normalizer==3.3.2
idna==3.7
pandas==2.2.2
python-dateutil==2.9.0
requests==2.32.3

pip freeze includes all transitive dependencies. For authoring a library or a pyproject.toml-based project, prefer poetry or uv which maintain a separate lockfile.

Deactivate

bash
deactivate

Output:

text
(prompt returns to normal — no (.venv) prefix)

Upgrade pip inside the venv

bash
pip install --upgrade pip

Output:

text
Requirement already satisfied: pip in ./.venv/lib/python3.12/site-packages (24.0)
Successfully installed pip-24.3.1

Delete and recreate

bash
rm -rf .venv
python -m venv .venv

Output: (none — exits 0 on success)

There are no lock files or metadata to clean up — just remove and recreate.

Quick reference table

TaskCommand
Createpython -m venv .venv
Activate (bash/zsh)source .venv/bin/activate
Activate (fish)source .venv/bin/activate.fish
Activate (PowerShell).venv\Scripts\Activate.ps1
Install packagepip install <pkg>
List installedpip list
Freeze to filepip freeze > requirements.txt
Install from filepip install -r requirements.txt
Deactivatedeactivate
Deleterm -rf .venv

Common pitfalls

Running without activation — if you python script.py without activating, you'll use the system Python and won't see your installed packages. Check which python before running anything.

PowerShell execution policy — if .venv\Scripts\Activate.ps1 fails with a policy error, run: Set-ExecutionPolicy -ExecutionPolicy RemoteSigned -Scope CurrentUser

Add .venv/ to your .gitignore. Never commit the virtual environment directory.

venv vs virtualenv vs uv venv vs conda

Python has several tools that create isolated environments. They serve overlapping needs with different tradeoffs in speed, language version flexibility, and ecosystem fit.

ToolSourceSpeedPython version mgmtBest for
python -m venvstdlib (Python 3.3+)Slow (~1s)No — uses caller's PythonDefault, no install needed
virtualenvthird-party (pip install virtualenv)SlowNo (pairs with pyenv)Older Pythons, custom site-packages
uv venvAstral uv (Rust binary)Fast (~50ms)Yes (uv python install)Modern projects, CI speed
conda envAnaconda / Miniconda / MiniforgeSlowYes (conda channels)Data science, non-Python deps
pyenv-virtualenvpyenv pluginSlowYes (pyenv-managed)Per-version venvs
pipenvthird-partySlowNoLegacy projects
poetry envpoetry-managedSlowNoPoetry projects

When to pick each:

  • python -m venv — zero install, always available, fine for most projects.
  • virtualenv — slightly faster than stdlib venv on cold start, supports older Pythons, lets you set a custom prompt prefix and seed packages.
  • uv venv — 20× faster, integrates with uv pip install, can download interpreters. Recommended for new projects.
  • conda — only when you need non-Python binaries (CUDA toolkit, MKL, R, geo libraries) that aren't on PyPI.

How venv works under the hood

python -m venv creates a directory tree that looks like a miniature Python installation. Activating it places that tree's bin/ (or Scripts\ on Windows) on PATH first, so python and pip resolve to copies inside the venv instead of the system-wide install.

text
.venv/
├── bin/                 (Linux/macOS) or Scripts/ (Windows)
│   ├── python           → symlink to the base interpreter
│   ├── python3          → symlink
│   ├── pip
│   ├── pip3
│   ├── activate         (bash/zsh)
│   ├── activate.fish
│   ├── activate.csh
│   └── Activate.ps1
├── include/             headers (mostly unused)
├── lib/
│   └── python3.12/
│       └── site-packages/   ← where installed packages live
└── pyvenv.cfg           ← config file pointing at the base interpreter

Inspect the config:

bash
cat .venv/pyvenv.cfg

Output:

text
home = /usr/bin
include-system-site-packages = false
version = 3.12.4
executable = /usr/bin/python3.12
command = /usr/bin/python3.12 -m venv /home/alice/myproject/.venv

Activation is just shell sugar around setting VIRTUAL_ENV, prepending .venv/bin to PATH, and updating the prompt. The Python interpreter inside the venv looks at pyvenv.cfg to find the base install for its standard library.

Activation by shell

Activation scripts ship in .venv/bin/ (Linux/macOS) and .venv\Scripts\ (Windows). Pick the file that matches your shell.

bash
# bash / zsh — POSIX shells
source .venv/bin/activate
. .venv/bin/activate                       # same thing, shorter

# fish
source .venv/bin/activate.fish

# csh / tcsh
source .venv/bin/activate.csh

# nushell
overlay use .venv/bin/activate.nu

# PowerShell (Windows + cross-platform pwsh)
.venv\Scripts\Activate.ps1                 # Windows
.venv/bin/Activate.ps1                     # macOS/Linux pwsh

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

# Git Bash on Windows
source .venv/Scripts/activate

Output: (none — exits 0 on success)

After activation, $VIRTUAL_ENV points to the venv directory and the prompt shows the venv name:

bash
echo $VIRTUAL_ENV

Output:

text
/home/alice/myproject/.venv

python -m venv options

python -m venv accepts flags that control what's seeded inside the new environment.

bash
python -m venv .venv                               # default
python -m venv --upgrade-deps .venv                # upgrade pip/setuptools after creation
python -m venv --system-site-packages .venv        # inherit system site-packages
python -m venv --without-pip .venv                 # skip pip install (rare)
python -m venv --copies .venv                      # copy files instead of symlinks
python -m venv --symlinks .venv                    # force symlinks (default on POSIX)
python -m venv --prompt myapp .venv                # custom shell prompt prefix
python -m venv --clear .venv                       # delete contents first
python -m venv --upgrade .venv                     # upgrade venv to current Python

Output: (none — exits 0 on success)

FlagWhen to use
--upgrade-depsAlways — ensures recent pip/setuptools
--system-site-packagesRare; mostly when leveraging a heavy system install (e.g., system PyTorch)
--without-pipBuild pipelines where pip is added separately
--copiesFilesystems that don't support symlinks (e.g., FAT, some network shares)
--promptCleaner prompt when the directory name is long
--clearRecreate without removing the directory itself
--upgradeAfter upgrading Python; relinks to the new interpreter

--system-site-packages

By default a venv is fully isolated from the system Python's site-packages. With --system-site-packages the venv can read packages installed system-wide but still installs its own packages locally.

bash
python -m venv --system-site-packages .venv
source .venv/bin/activate
python -c "import sys; print('\n'.join(sys.path))"

Output:

text
/home/alice/myproject/.venv/lib/python3.12/site-packages
/usr/lib/python3.12
/usr/lib/python3.12/lib-dynload
/usr/lib/python3/dist-packages           ← system site-packages added back

--system-site-packages defeats most of the isolation venv is supposed to provide. Use it only when the system install includes something heavy and stable you don't want to duplicate.

Multiple Python versions

python -m venv always uses the interpreter that ran it. To create venvs with different versions, invoke the version explicitly.

bash
python3.10 -m venv .venv-py310
python3.11 -m venv .venv-py311
python3.12 -m venv .venv-py312

# macOS Homebrew typical paths
/opt/homebrew/bin/python3.12 -m venv .venv

# pyenv-managed
~/.pyenv/versions/3.12.4/bin/python -m venv .venv

# uv-managed
uv venv --python 3.12 .venv-py312

Output: (none — exits 0 on success)

To recreate a venv with a different Python after the fact, delete and recreate — venv has no in-place version switch.

site-packages structure

site-packages is the directory each installed package lands in. Inspecting it is the fastest way to understand what's actually installed.

bash
ls .venv/lib/python3.12/site-packages/

Output:

text
__pycache__/
_distutils_hack/
certifi/
certifi-2024.2.2.dist-info/
charset_normalizer/
charset_normalizer-3.3.2.dist-info/
idna/
idna-3.7.dist-info/
pip/
pip-24.1.dist-info/
requests/
requests-2.32.3.dist-info/
urllib3/
urllib3-2.2.1.dist-info/

Each installed package has:

  • A package directory (requests/) containing the importable Python modules.
  • A .dist-info/ metadata directory with METADATA, RECORD, WHEEL, entry_points.txt, etc.

Find which file belongs to which package:

bash
grep -l "^requests/" .venv/lib/python3.12/site-packages/*.dist-info/RECORD

Output:

text
.venv/lib/python3.12/site-packages/requests-2.32.3.dist-info/RECORD

ensurepip — bootstrapping pip

ensurepip is the stdlib module that installs pip into a Python environment. It is what python -m venv invokes internally and what you reach for when pip has been removed or never installed.

bash
python -m ensurepip                       # install pip if missing
python -m ensurepip --upgrade             # upgrade bundled pip
python -m ensurepip --default-pip         # symlink `pip` to point at this Python's pip

Output:

text
Looking in links: /tmp/tmpxyz
Requirement already satisfied: setuptools in /home/alice/.venv/lib/python3.12/site-packages (69.5.1)
Requirement already satisfied: pip in /home/alice/.venv/lib/python3.12/site-packages (24.0)

On Debian/Ubuntu, the system Python ships without ensurepip by default — install python3-venv and python3-pip packages first.

pip-tools workflow inside venv

pip-tools is the canonical way to maintain a pinned requirements.txt from a human-edited requirements.in. It's the closest equivalent to a uv.lock or poetry.lock when you must stay on plain pip + venv.

bash
source .venv/bin/activate
pip install pip-tools

# requirements.in — direct deps only
cat > requirements.in <<EOF
fastapi
sqlalchemy>=2
pytest
EOF

pip-compile requirements.in              # → requirements.txt (fully pinned)
pip-compile --upgrade requirements.in    # refresh transitive deps
pip-compile --generate-hashes requirements.in   # add SHA-256 hashes

pip-sync requirements.txt                # make venv exactly match the file

Output of pip-compile:

text
#
# This file is autogenerated by pip-compile with Python 3.12
# by the following command:
#
#    pip-compile requirements.in
#
fastapi==0.111.1
sqlalchemy==2.0.30
pytest==8.2.2
...

pip-sync is the missing piece pip lacks natively — it installs anything in requirements.txt, upgrades stale packages, and removes anything not listed.

Deactivation and cleanup

bash
deactivate                # exits the venv (back to system Python)
rm -rf .venv              # delete the venv entirely

Output: (none — exits 0 on success)

deactivate is a shell function injected by the activation script. It restores PATH, removes VIRTUAL_ENV, and unsets the prompt prefix. There is no state to clean up — closing the shell achieves the same thing.

If deactivate is missing, the shell wasn't activated. If activation looks active but which python still shows the system Python, the activation script likely failed silently (most common on Windows PowerShell — see execution policy below).

.gitignore patterns

text
# .gitignore — venv directories
.venv/
venv/
env/
.env/
ENV/
env.bak/
venv.bak/

# pyenv-virtualenv local file
.python-version

# pip-tools artifacts (optional — usually kept)
# requirements.txt

Never commit a venv directory. They contain absolute paths in shebangs, are platform-specific, and can be hundreds of MB.

Devcontainer integration

A .devcontainer/devcontainer.json plus a postCreateCommand is the standard way to bootstrap a venv in VS Code or GitHub Codespaces.

json
{
  "image": "mcr.microsoft.com/devcontainers/python:1-3.12-bookworm",
  "features": {},
  "postCreateCommand": "python -m venv .venv && .venv/bin/pip install --upgrade pip && .venv/bin/pip install -r requirements.txt",
  "customizations": {
    "vscode": {
      "settings": {
        "python.defaultInterpreterPath": ".venv/bin/python",
        "python.terminal.activateEnvironment": true
      },
      "extensions": ["ms-python.python", "charliermarsh.ruff"]
    }
  }
}

VS Code auto-discovers .venv next to pyproject.toml or requirements.txt and offers to use its interpreter — set python.defaultInterpreterPath to make that automatic.

Docker integration

Two patterns are common: a venv inside the image, or installing directly into the system Python of a slim base image.

Pattern 1 — venv inside the image (preferred)

dockerfile
FROM python:3.12-slim

# Create venv
RUN python -m venv /opt/venv
ENV PATH="/opt/venv/bin:$PATH"

# Install deps
COPY requirements.txt .
RUN pip install --no-cache-dir -r requirements.txt

# Copy app
COPY . /app
WORKDIR /app
CMD ["python", "-m", "myapp"]

The venv at /opt/venv is isolated from the system Python — easy to copy to a smaller runtime image in a multi-stage build.

Pattern 2 — multi-stage with venv copy

dockerfile
FROM python:3.12 AS builder
RUN python -m venv /opt/venv
ENV PATH="/opt/venv/bin:$PATH"
COPY requirements.txt .
RUN pip install --no-cache-dir -r requirements.txt

FROM python:3.12-slim
COPY --from=builder /opt/venv /opt/venv
ENV PATH="/opt/venv/bin:$PATH"
COPY . /app
WORKDIR /app
CMD ["python", "-m", "myapp"]

The builder stage has compilers for native dependencies; the final image is much smaller.

Don't source activate inside a Dockerfile — each RUN is a separate shell and activation does not persist. Instead, prepend /opt/venv/bin to PATH once via ENV.

Activation aliases

A common shell setup is to alias venv creation and activation to a single keystroke. Drop these in ~/.bashrc, ~/.zshrc, or ~/.config/fish/config.fish.

bash
# bash/zsh — create and activate
mkvenv() {
  python -m venv .venv --upgrade-deps --prompt "$(basename "$PWD")"
  source .venv/bin/activate
}

# Auto-activate when entering a directory with .venv
chpwd() {
  if [[ -f .venv/bin/activate ]]; then
    source .venv/bin/activate
  fi
}

Output: (none — exits 0 on success)

fish
# fish
function mkvenv
    python -m venv .venv --upgrade-deps --prompt (basename (pwd))
    source .venv/bin/activate.fish
end

function __auto_venv --on-variable PWD
    if test -f .venv/bin/activate.fish
        source .venv/bin/activate.fish
    end
end

Output: (none — exits 0 on success)

For a battle-tested version, install direnv and drop layout python into .envrc — it auto-activates .venv on cd and deactivates on leave.

Common pitfalls (additional)

Hardcoded shebangs break on rename — every script in .venv/bin/ has a #!/full/path/to/.venv/bin/python shebang. If you move the venv directory, scripts will fail. Recreate the venv instead of moving it.

System Python upgrades break the venv — if the OS upgrades Python 3.12.3 → 3.12.4 and removes 3.12.3's stdlib, your venv's symlinked interpreter may break. Recreate with python -m venv --upgrade .venv after major system updates.

Mixing venvs with sudo — never run sudo pip install after activating a venv. sudo resets PATH and you'll install into the system Python instead of the venv, with root-owned files inside ~/.local.

Use --prompt myapp so the prompt shows (myapp) instead of (.venv). When five terminals all have venvs active, named prompts save your sanity.

If Activate.ps1 fails with an execution policy error: Set-ExecutionPolicy -ExecutionPolicy RemoteSigned -Scope CurrentUser.

Real-world recipes

Recipe — fresh project with venv + pip-tools

bash
mkdir myproject && cd myproject
python -m venv .venv --upgrade-deps --prompt myproject
source .venv/bin/activate
pip install pip-tools
cat > requirements.in <<EOF
fastapi
sqlalchemy>=2
EOF
pip-compile --generate-hashes requirements.in
pip-sync requirements.txt
echo ".venv/" >> .gitignore

Output: (none — exits 0 on success)

Recipe — share a venv between siblings

When two projects in a monorepo need the same dependencies, point both at one venv via pyvenv.cfg:

bash
python -m venv ~/.venvs/shared
ln -s ~/.venvs/shared myproject/.venv
ln -s ~/.venvs/shared sibling-project/.venv

Output: (none — exits 0 on success)

A symlinked .venv keeps each project's directory layout clean while sharing the actual install — useful for slow-installing data-science stacks.

Recipe — recreate from scratch in CI

bash
rm -rf .venv
python -m venv .venv --upgrade-deps
.venv/bin/pip install --no-cache-dir -r requirements.txt
.venv/bin/python -m pytest

Output:

text
Successfully installed pip-24.1 setuptools-69.5.1
Successfully installed fastapi-0.111.1 pydantic-2.7.4 sqlalchemy-2.0.30 ...
============================== 42 passed in 1.21s ==============================

--no-cache-dir avoids leaving wheel cache in CI workspaces that aren't cached anyway.

Recipe — venv inside WSL with VS Code on Windows

bash
# Inside WSL Ubuntu
sudo apt update && sudo apt install -y python3.12 python3.12-venv
cd /mnt/c/Users/alice/code/myproject
python3.12 -m venv .venv
source .venv/bin/activate
pip install -r requirements.txt

Output: (none — exits 0 on success)

Open the project in VS Code via code . from WSL; the Python extension picks up .venv/bin/python automatically.

Recipe — switch venvs with direnv

bash
# .envrc in project root
layout python3.12

Output: (none — exits 0 on success)

bash
direnv allow
# direnv auto-creates .direnv/python-3.12/ and activates on cd

Output:

text
direnv: loading ~/code/myproject/.envrc
direnv: export +VIRTUAL_ENV ~PATH

direnv replaces manual source .venv/bin/activate — entering the directory activates, leaving deactivates.

Quick reference (extended)

TaskCommand
Create with version pinpython3.12 -m venv .venv
Create with upgraded pippython -m venv --upgrade-deps .venv
Create with custom promptpython -m venv --prompt myapp .venv
Activate (bash/zsh)source .venv/bin/activate
Activate (fish)source .venv/bin/activate.fish
Activate (PowerShell).venv\Scripts\Activate.ps1
Activate (cmd.exe).venv\Scripts\activate.bat
Show active venvecho $VIRTUAL_ENV
Show interpreter pathwhich python / where.exe python
Install packagepip install <pkg>
Compile pinned reqspip-compile requirements.in
Sync to requirementspip-sync requirements.txt
List installedpip list
Deactivatedeactivate
Deleterm -rf .venv (Remove-Item -Recurse .venv on PS)
Upgrade venv to current Pythonpython -m venv --upgrade .venv