Every Python project needs isolated dependencies, but the ecosystem has accumulated four competing tools for it. Here's how they actually compare.
venv — the built-in baseline
python -m venv .venv
source .venv/bin/activate
pip install -r requirements.txtvenv ships with Python itself. It's the lowest common denominator: no lockfile, no dependency resolution beyond pip's, but zero extra install required. Good for small scripts and quick experiments.
pipenv — venv plus a lockfile
pipenv install requests
pipenv shellAdds a Pipfile.lock for reproducible installs and merges virtual env management with dependency management. Largely superseded by poetry and uv in new projects, but still common in legacy codebases.
poetry — dependency management and packaging
poetry init
poetry add requests
poetry run python main.pyPoetry handles dependency resolution, lockfiles, and package publishing in one tool. It's a solid default for libraries you intend to publish to PyPI.
uv — the fast newcomer
uv venv
uv pip install requestsuv (from the makers of Ruff) reimplements the pip/venv workflow in Rust and is often 10-100x faster for dependency resolution and installs. It's rapidly becoming the default recommendation for new projects in 2026.
Recommendation
| Situation | Use |
|---|---|
| Quick script, no team | venv + pip |
| Publishing a library to PyPI | poetry |
| New project, want speed | uv |
| Maintaining a legacy project | Whatever it already uses |
If you're starting a new project today with no constraints, uv is the pragmatic default — it's compatible with existing requirements.txt and pyproject.toml conventions, so switching later isn't a rewrite.
- Python 3.9+ installed and available on your
PATH - Basic familiarity with the terminal and
pip - For
uv: no separate install of Python required —uvcan install and manage Python versions itself
Once your environment is isolated and reproducible, the same discipline pays off directly the moment you start writing async Python with asyncio — a clean, pinned dependency set is one less variable when debugging why concurrent code behaves differently than expected.
What actually happens when you "activate" a venv
source .venv/bin/activate doesn't install anything or change files on disk — it modifies your current shell session's PATH (and a couple of related environment variables) so that python and pip resolve to the binaries inside .venv instead of the system ones:
which python
# before activation: /usr/bin/python3
# after activation: /path/to/project/.venv/bin/pythonThis is why activation is per-shell-session, not global or persistent — opening a new terminal tab starts fresh with the system Python again, and needs its own activate call. It's also why "activating" doesn't require any special permissions or system-level changes: it's a shell-local PATH change, nothing more, which is exactly what makes venvs cheap enough to create one per project without worrying about a limited system-wide resource.
Reproducing an environment on a new machine
The lockfile each tool generates is what turns "works on my machine" into "works on every machine" — installing from it, rather than a loosely-versioned requirements.txt, pins exact versions of every transitive dependency, not just the ones you explicitly listed:
# venv/pip
pip install -r requirements.txt
# poetry
poetry install
# uv
uv syncThe distinction that matters: a plain requirements.txt with unpinned or loosely-pinned versions (requests>=2.0) can resolve to different actual versions on different days as new releases publish — a real lockfile (poetry.lock, uv.lock, or a pip freeze-generated requirements.txt) pins exact versions for every package in the dependency tree, guaranteeing the same install every time regardless of when it runs.
uv's Python version management
Beyond package installation speed, uv can also install and manage Python interpreter versions themselves, similar to what pyenv does separately for the other tools:
uv python install 3.12
uv venv --python 3.12This collapses two previously separate concerns (which Python interpreter, which packages) into one tool, which is part of why it's become the default recommendation for new projects — one install, one tool, one lockfile format, instead of coordinating pyenv and a separate package manager together.
Common mistakes
- Installing packages with a global
pip installbefore activating a virtual environment — this silently pollutes your system Python instead of the project's isolated one. Always confirmwhich python(orwhere pythonon Windows) points inside.venvbefore installing anything. - Committing the
.venv/venvfolder to git. It's large, machine-specific, and unnecessary — commit the lockfile instead, and add the environment folder to.gitignore. - Running two isolation tools at once (e.g. activating a
venvinside an already-activecondaenvironment). Package resolution gets confused about which environment actually owns a given install. - Pinning exact versions in
requirements.txtby hand instead of generating it from an actual resolved environment (pip freeze > requirements.txt) — hand-written pins drift from what's actually installed and reintroduce the reproducibility problem venv was meant to solve.
Troubleshooting
- "command not found: python" — many systems (especially Linux) only expose
python3, notpython, for the system interpreter. Usepython3 -m venv .venv, or alias it if your workflow expectspython. - Wrong Python version inside the venv — a virtual environment inherits whichever interpreter created it. If
python -m venv .venvpicks up an unexpected version, checkpyenv versions(if you use pyenv) or specify the interpreter explicitly:python3.12 -m venv .venv. ModuleNotFoundErrorright after installing a package — almost always means the venv isn't actually activated, or you have a second venv shadowing it. Check your shell prompt for the(.venv)prefix, and confirm withwhich python.uvorpoetrycommand not found after install — both install to a user-local bin directory that may not be on yourPATHby default; their installers print the exact line to add, and it's worth actually reading that output rather than skipping past it.
Related reading
- Async Python with asyncio: A Practical Introduction — shares tags: python, programming (same category).
- Big O Notation Without the Math Panic — shares tags: programming, productivity.
- Clean Code Principles That Actually Hold Up in Practice — shares tags: programming, productivity.
- Understanding Cloud Cost Optimization Basics — shares tags: productivity.
- Core Web Vitals Explained: What Actually Affects Your Score — shares tags: programming.