Virtual Environments
1 · The lesson
readEvery Python project should live inside its own virtual environment. No exceptions. The moment you pip install something globally is the moment you start collecting compatibility problems that take weeks to untangle.
The problem is concrete — Project A needs requests==2.31. Project B is an older codebase that still depends on requests==2.20. Both can't coexist in your system Python. A virtual environment (venv) is a self-contained Python installation that lives inside a project folder, with its own python, its own pip, and its own site-packages. Each project gets one. They never talk to each other.
This lesson covers venv, the modern uv replacement, pipx for CLI tools, lockfiles, and the conventions that prevent your laptop turning into a dependency landfill.
1. venv — The Standard Library Tool
Every Python 3 install ships with venv. No third-party tool required.
python -m venv .venv
That creates a .venv/ directory in the current folder containing a Python binary, a fresh pip, and an empty site-packages. The convention is to name the directory .venv (leading dot, hidden in ls) so it's easy to gitignore and easy to spot.
2. Activate, Use, Deactivate
The activation script is different per shell.
macOS / Linux:
source .venv/bin/activate
Windows PowerShell:
.venv\Scripts\Activate.ps1
Windows cmd:
.venv\Scripts\activate.bat
Your prompt changes to show the active venv — typically (.venv) $. Now python and pip point at the venv's copies, not the system ones:
which python # /path/to/project/.venv/bin/python (macOS/Linux) where python # ...\project\.venv\Scripts\python.exe (Windows) python -V # the Python version inside the venv pip list # only what you've installed here
Install packages — they land in .venv/lib/.../site-packages, nowhere else:
pip install requests pytest
Deactivate when you're done:
deactivate
The venv directory still exists; activation is just a PATH change. Re-activate whenever you come back to the project.
3. The Single Most Important Rule
.venv/ must be gitignored. Always. Without exception.
# .gitignore .venv/ venv/ env/
A venv contains absolute paths baked into its scripts, platform-specific binaries, and roughly 100 MB of downloaded packages. None of it is portable. Committing it bloats your repo, breaks for every other developer (different paths, different OS), and adds nothing — the venv is reproducible from your dependency file in seconds.
4. Recording and Restoring Dependencies
A venv is recreated from a list, not committed. Two paths:
The classic — requirements.txt:
pip freeze > requirements.txt
# requirements.txt certifi==2024.2.2 charset-normalizer==3.3.2 idna==3.7 requests==2.31.0 urllib3==2.2.1
pip freeze dumps every installed package, including transitive dependencies, at exact versions. To recreate the environment elsewhere:
python -m venv .venv source .venv/bin/activate pip install -r requirements.txt
The modern — pyproject.toml:
If your project has a pyproject.toml (see the packaging lesson), declare dependencies there and install editable:
pip install -e ".[dev]"
This is the better path for any project that will eventually be a package. requirements.txt still has its place — typically as a lockfile generated from pyproject.toml for reproducible installs.
5. Lockfiles — Reproducible Installs
requirements.txt from pip freeze is a snapshot, not a lockfile. It captures versions but not hashes, and doesn't separate direct dependencies from transitive ones.
For real reproducibility, use a lockfile tool:
| Tool | Lockfile | Notes |
|---|---|---|
| pip-tools | requirements.txt (compiled) | pip-compile reads pyproject.toml (or requirements.in) and produces a fully-pinned, hash-locked requirements.txt. |
| poetry | poetry.lock | Bundled with the poetry CLI. Locks transitive deps, supports groups. |
| uv | uv.lock | Fastest. Reads pyproject.toml, locks with hashes, syncs deterministically. |
| pdm | pdm.lock | PEP 582-friendly, similar feature surface to poetry. |
A lockfile guarantees that pip install -r requirements.txt (or uv sync) on your colleague's machine, in CI, and in production produces the exact set of packages and versions you tested with. For libraries, you don't need this — loose ranges win. For applications, you do.
6. uv — The Modern Fast Replacement
uv is a Rust-based tool that does everything pip and venv do, ten to a hundred times faster. It's the new default for new projects.
# install once (Homebrew, pipx, or the official installer script) brew install uv # macOS pipx install uv # cross-platform # inside a project: uv venv # create .venv (faster than python -m venv) uv pip install requests # like pip install, but fast uv pip install -r requirements.txt uv sync # install + lock from pyproject.toml uv add requests # add a dep to pyproject AND install it uv run pytest # run a command in the project's venv, no activation needed
uv run is the killer feature — you don't even have to activate the venv. It detects the project, uses (or creates) the right venv, and runs your command. For day-to-day work, that single command replaces the entire activate/run/deactivate dance.
uv also manages Python versions:
uv python install 3.12 # download a Python interpreter uv venv --python 3.12 # create a venv using that version
Recommendation: default to uv for new projects. Stick with stock venv + pip if you're contributing to a project that hasn't adopted it.
7. pipx — Global CLI Tools Done Right
Some packages are tools, not libraries — black, ruff, httpie, poetry, pre-commit. You want them available everywhere, but you don't want them dumped into your system Python, and putting them in every project venv is silly.
pipx solves this. It installs each tool into its own isolated venv and links the entry point onto your PATH.
pipx install black pipx install ruff pipx install httpie pipx install uv pipx list # see what's installed pipx upgrade-all
You get black on your PATH everywhere, with no contamination of any project or system Python. This is the right tool for end-user applications; venv is the right tool for project dependencies. Don't confuse them.
8. conda / mamba — When You Need Non-Python Deps
conda (and the faster reimplementation mamba) is a different kind of environment manager. It handles non-Python dependencies — compiled libraries, CUDA toolkits, R, system tools. Mostly relevant in scientific computing and ML.
conda create -n myenv python=3.12 numpy scipy pytorch conda activate myenv
For pure-Python projects, conda is overkill — venv or uv is lighter, faster, and integrates better with the rest of the Python ecosystem. Reach for conda when you genuinely need its non-Python package handling (NumPy with Intel MKL, CUDA-linked PyTorch, GDAL with its system libraries).
9. poetry — The All-In-One
poetry bundles env management, dependency resolution, lockfiles, and publishing into a single CLI:
poetry new myproject poetry add requests poetry install poetry run pytest poetry publish
It's opinionated, well-loved by some, and slower than uv. If you're starting fresh today, uv covers the same ground and runs faster. Poetry is still a reasonable choice if you prefer its workflow — just know it's no longer the obvious modern pick.
10. Python Version Management
Multiple Python versions on one machine is the norm — Python 3.10 for one client's legacy code, 3.12 for a new project, the system Python you should never touch. Use a version manager:
pyenv(macOS / Linux) — installs and switches Python versions per-project via a.python-versionfile.pyenv-win— the Windows fork.uv python install— built intouv. Simplest if you already use uv.
pyenv install 3.12.3 pyenv local 3.12.3 # writes .python-version in the current dir python -V # Python 3.12.3
What you should not use as your project's Python: the system Python that ships with macOS or comes preinstalled on Linux. Those are the OS's Python — upgrading or breaking them breaks the OS. Always install your own.
11. The Directory Pattern
The convention that scales:
~/code/
project-a/
.venv/ # project-a's venv, gitignored
pyproject.toml
src/
project-b/
.venv/ # project-b's venv, gitignored
pyproject.toml
src/Every project gets its own .venv inside the project directory. Never share a venv across projects. Never store venvs in a central location like ~/venvs/ — when you delete the project, the venv goes with it; when you move the project, you don't have to update tool configs pointing at a separate venv path.
IDE tip: set the Python interpreter to .venv/bin/python (or .venv\Scripts\python.exe on Windows) explicitly. VS Code and PyCharm auto-detect this if the .venv is at the project root.
Common Mistakes
1. No venv at all
You pip install straight into the system Python. Six months later a different project upgrades a shared dependency, and the original project mysteriously breaks. This is the entire reason venvs exist. Use one from day one — python -m venv .venv is fifteen seconds.
2. Committing .venv/
A 100 MB diff with absolute paths baked into shebangs. Won't even work on a colleague's machine — their Python lives at a different path. Add .venv/ to .gitignore immediately.
3. Mixing global and project installs
pip install pandas # was the venv active? unclear
Before any pip install, check which python (or where python on Windows). If it doesn't say .venv, you're not in the venv. The venv name in your prompt is the visual confirmation — (.venv) $ means safe, no prefix means you're about to install globally.
4. Forgetting to activate before installing
Same root cause as the previous one. Easy to do after switching directories or opening a new terminal. which python is the muscle memory.
5. requirements.txt without pinned versions
requests pandas
Works today. Two weeks later, pandas 3.0 releases with breaking changes and your CI explodes. Pin to known-good versions:
requests==2.31.0 pandas==2.2.1
For libraries, pin loosely (requests>=2.30,<3); for applications, pin tightly or use a lockfile.
6. Multiple Pythons on PATH, wrong one creates the venv
python -m venv .venv uses whichever python runs first on your PATH. If that's the wrong version, every install after that targets the wrong Python. Be explicit:
python3.12 -m venv .venv # or with uv: uv venv --python 3.12
7. venv name collisions in IDEs
PyCharm sometimes auto-creates venv/ while you've made .venv/. Now you have two venvs and the IDE uses one, your terminal uses the other. Pick one name, configure the IDE to use that exact path, delete the duplicate.
🎯 Your Turn — A Setup Script
Write a setup script that bootstraps a fresh dev environment in one command. Both shells, because cross-platform matters.
Requirements — your script should:
1. Create a .venv/ in the current directory using python -m venv .venv.
2. Activate it for the script's session.
3. Upgrade pip to the latest version.
4. Install everything from requirements.txt.
5. Print a final "done — activate with …" hint.
Write setup.sh (bash, for macOS/Linux) and setup.ps1 (PowerShell, for Windows).
Skeleton — setup.sh:
#!/usr/bin/env bash set -euo pipefail # TODO 1: create .venv # TODO 2: activate it # TODO 3: upgrade pip # TODO 4: install requirements.txt if it exists # TODO 5: print a friendly hint
Skeleton — setup.ps1:
$ErrorActionPreference = "Stop" # TODO 1-5 as above, with PowerShell syntax
Hint 1 — Activation inside a script
In bash,source .venv/bin/activate works because the script runs in a single shell. In PowerShell, . .\.venv\Scripts\Activate.ps1 dot-sources the activation script. After activation, pip automatically points at the venv's copy.
Hint 2 — Conditional file check
Bash:if [ -f requirements.txt ]; then pip install -r requirements.txt; fi. PowerShell: if (Test-Path requirements.txt) { pip install -r requirements.txt }.
Show full solution
setup.sh — bash (macOS / Linux):
#!/usr/bin/env bash
set -euo pipefail
PYTHON="${PYTHON:-python3}"
echo "==> Creating .venv with $PYTHON"
$PYTHON -m venv .venv
# shellcheck disable=SC1091
source .venv/bin/activate
echo "==> Upgrading pip"
python -m pip install --upgrade pip
if [ -f requirements.txt ]; then
echo "==> Installing requirements.txt"
pip install -r requirements.txt
elif [ -f pyproject.toml ]; then
echo "==> Installing project (editable, with dev extras)"
pip install -e ".[dev]" || pip install -e .
else
echo "==> No requirements.txt or pyproject.toml found — skipping install."
fi
echo ""
echo "Done. Activate the venv with:"
echo " source .venv/bin/activate"Make it executable and run:
chmod +x setup.sh ./setup.sh
setup.ps1 — PowerShell (Windows):
$ErrorActionPreference = "Stop"
$Python = if ($env:PYTHON) { $env:PYTHON } else { "python" }
Write-Host "==> Creating .venv with $Python"
& $Python -m venv .venv
Write-Host "==> Activating .venv"
. .\.venv\Scripts\Activate.ps1
Write-Host "==> Upgrading pip"
python -m pip install --upgrade pip
if (Test-Path requirements.txt) {
Write-Host "==> Installing requirements.txt"
pip install -r requirements.txt
} elseif (Test-Path pyproject.toml) {
Write-Host "==> Installing project (editable, with dev extras)"
try { pip install -e ".[dev]" } catch { pip install -e . }
} else {
Write-Host "==> No requirements.txt or pyproject.toml found — skipping install."
}
Write-Host ""
Write-Host "Done. Activate the venv with:"
Write-Host " .\.venv\Scripts\Activate.ps1"Run it (you may need to allow script execution once):
Set-ExecutionPolicy -Scope CurrentUser -ExecutionPolicy RemoteSigned .\setup.ps1
A clean clone of the project now becomes one command for any new developer — and the script is short enough to read top-to-bottom and trust. Bonus points if you commit it alongside the project's README, with a single line: "Run ./setup.sh (or .\setup.ps1) and you're done."
What You Learned
- A virtual environment is a per-project Python install with its own
site-packages. One per project, always. - Create with
python -m venv .venv. Activate withsource .venv/bin/activate(orActivate.ps1on Windows). Deactivate withdeactivate. - Always gitignore
.venv/. Recreate fromrequirements.txtorpyproject.toml, never commit the directory. pip freeze > requirements.txtcaptures a snapshot. Lockfiles (pip-tools,uv.lock,poetry.lock) give true reproducibility.uvis the modern, fast, Rust-based replacement forpip+venv.uv runremoves the activation step entirely.pipxinstalls CLI tools (black,ruff,httpie) in their own isolated venvs — global availability without contamination.condais for non-Python dependencies (CUDA, MKL, GDAL); skip it for pure-Python projects.- Use a Python version manager (
pyenv,uv python) rather than the system Python. - Before every
pip install, checkwhich python— if it doesn't show.venv, you're about to install globally.
Next: round out the How-to path with deployment and ops topics, or revisit the packaging lesson to publish your first project to PyPI.