Overview
Every Python project needs its dependencies isolated from every other project's, because a shared global installation can only ever hold one version of any given package, a guaranteed conflict the moment two projects need different versions of the same dependency. Virtual environments solve isolation; lockfiles solve reproducibility on top of that, pinning the exact resolved version of every dependency (including transitive ones) so an install produces identical results on any machine, not just "probably compatible" ones. Modern tools (Poetry, uv) bundle both concerns into one workflow, but understanding what venv and a lockfile each solve independently makes it clear why skipping either one causes real, recurring problems.
Quick Reference
| Tool/File | Solves |
|---|---|
venv | Per-project isolated package installation |
requirements.txt | Declares direct dependencies (often loosely versioned) |
Lockfile (poetry.lock, uv.lock) | Pins exact resolved versions of every dependency, including transitive ones |
pyproject.toml | Modern, standardized project/dependency metadata |
Syntax
python -m venv .venv
source .venv/bin/activate # macOS/Linux
pip install -r requirements.txt# Modern equivalent with a lockfile-based tool
uv venv
uv add requests
uv syncExamples
# requirements.txt - loose version range, not fully reproducible
requests>=2.28# pyproject.toml + a generated lockfile - exact versions pinned,
# fully reproducible across machines and time.
[project]
dependencies = ["requests>=2.28"]A requirements.txt alone is not a lockfile
Loose version ranges mean two installs weeks apart can silently resolve to different actual versions. Use pip freeze > requirements.txt for a pinned snapshot, or better, a proper lockfile-based tool that also locks transitive dependencies.
Visual Diagram
Common Mistakes
- Installing packages globally (
pip installwithout an active virtual environment), causing version conflicts across unrelated projects. - Treating
requirements.txtwith loose version ranges as sufficient for reproducibility; it isn't, since resolved versions can drift over time. - Committing a virtual environment directory (
.venv/) to version control instead of just the dependency manifest and lockfile; it's large, platform-specific, and regeneratable. - Forgetting to activate the virtual environment before installing a new package, silently installing it globally instead.
Performance
- Virtual environment creation and activation have effectively zero runtime performance cost; the overhead is entirely at setup/install time, not at execution time.
- Dependency resolution (computing a consistent set of versions across a full dependency tree) can be slow for projects with many dependencies; lockfile-based tools cache resolution results specifically to avoid repeating that work on every install.
- Installing from a lockfile is generally faster than fresh resolution, since the resolver's work is already done and recorded.
Best Practices
- Use a virtual environment for every project, without exception, even small scripts; the isolation cost is near zero and the alternative failure mode is real.
- Commit a lockfile alongside dependency declarations so every install, on every machine, resolves identically.
- Never commit the virtual environment directory itself, only the manifest and lockfile needed to recreate it.
- Regenerate and review lockfile diffs during dependency updates, the same way you'd review any other code change, since transitive dependency changes can introduce real behavior differences.