Server

virtualenv vs venv vs uv: Python Environments in 2026

By Prabath Thalangama· October 24, 2026· 3 min read
#python#packaging#tooling

Introduction

A virtual environment is a directory with its own site-packages and a python symlink, so one project's dependencies don't collide with another's or with the system Python. Several tools create them; they differ in speed and in what else they manage.

What each is

  • venv — standard library (python -m venv). Creates an environment. No dependency resolution beyond pip, no lockfile, no Python-version management. Always available, zero install.
  • virtualenv — the third-party predecessor to venv. Slightly faster creation, supports older Pythons, a few more options. Rarely needed now.
  • uv — a fast (Rust) all-in-one: creates environments, resolves and installs dependencies, writes a lockfile, manages pyproject.toml, and can install Python interpreters itself. A drop-in for pip + venv + pip-tools + pyenv.

The plain venv workflow

python -m venv .venv
source .venv/bin/activate          # .venv\Scripts\activate on Windows
pip install -r requirements.txt
pip freeze > requirements.txt      # crude lock — pins everything, no hashes
deactivate

Works everywhere. Weakness: pip freeze mixes direct and transitive deps and doesn't separate "what I asked for" from "what got resolved".

Better: pyproject.toml + a real lock

pyproject.toml declares your direct dependencies; a lockfile pins the full resolved tree with hashes.

With uv

uv init myproject && cd myproject
uv add requests 'fastapi>=0.110'
uv add --dev pytest ruff
uv run pytest                       # runs in the managed env, auto-syncing first
uv sync                             # make .venv match uv.lock exactly
uv lock --upgrade                   # bump the lock deliberately

uv.lock is committed. uv sync --frozen in CI installs exactly the lock, fast. uv python install 3.12 grabs an interpreter — no pyenv needed.

With pip-tools (if you can't adopt uv)

# requirements.in has your direct deps
pip-compile requirements.in                 # -> requirements.txt, fully pinned + hashes
pip-sync requirements.txt                    # make the env match exactly

Reproducibility rules

  • Commit the lockfile (uv.lock, poetry.lock, compiled requirements.txt).
  • Never commit .venv/ — it's machine-specific; .gitignore it.
  • CI installs from the lock, frozen — a CI run should fail if the lock is stale, not silently resolve something new.
  • Pin the Python version too (.python-version, or requires-python + uv python pin).

Verification and troubleshooting

which python; python -c "import sys; print(sys.prefix)"   # is the venv active?
pip --version                                             # which pip
python -m pip list
uv pip list
  • pip install hit the system Python — venv not activated, or an IDE using a different interpreter. Check sys.prefix.
  • "works on my machine" — no lockfile, or CI didn't install --frozen. Someone's transitive dep floated to a new version.
  • source: no such file on activate — env created with a different Python that's since moved, or you're in the wrong shell (activate.fish, Activate.ps1). Recreate: rm -rf .venv && uv venv / python -m venv .venv.
  • Slow installs in CI — no wheel cache. uv caches aggressively by default; for pip, cache ~/.cache/pip (see GitHub Actions cache strategies).
  • uv and an existing .venv disagree — uv sync reconciles to the lock; if you hand-pip installed into it, uv will remove the extras.
  • Hash mismatch on install — the lockfile hashes don't match the downloaded wheel (mirror issue, or a re-uploaded package). Re-lock.
  • Multiple Python versions confusion — uv run --python 3.11 ... or py -3.12 -m venv (Windows launcher) to be explicit.
PrabathStuck on something this site can't fix?Reach out to Prabath directly on LinkedIn.