Virtual Environments and Tooling

venv, pip and requirements.txt, and a look at modern tools like uv and poetry.

Why isolate dependencies at all

Every non-trivial Python project depends on third-party packages, and different projects on the same machine almost never want the same versions of those packages. One project needs django==4.2, another needs django==5.1; one needs an old requests release because a client's API only works with it. If every package installed with pip landed in one shared, global location, you couldn't have both projects on the same machine at once without one of them breaking.

A virtual environment solves this by giving each project its own private, isolated folder of installed packages and its own copy of the python/pip executables that point at that folder instead of the system-wide installation. Activating a project's virtual environment before working on it — and never installing packages globally with pip install outside of one — is the single most important professional habit in Python tooling, and it's expected in every real codebase you'll touch.

venv — the built-in tool

venv ships with every Python 3 installation, so it needs no separate install. Create one inside a project folder (the conventional name is .venv or venv):

Bash
cd my-project
python3 -m venv .venv

This creates a .venv/ directory containing a private Python interpreter and an empty site-packages folder for that project's dependencies alone. Creating it doesn't change anything about your shell yet — you have to activate it:

Bash
# macOS / Linux
source .venv/bin/activate

# Windows (PowerShell)
.venv\Scripts\Activate.ps1

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

Once activated, your shell prompt usually shows the environment's name (e.g. (.venv) $), and — crucially — python and pip now resolve to the copies inside .venv, not the system ones:

Bash
which python
# /path/to/my-project/.venv/bin/python   (not /usr/bin/python3)

pip install requests
# installs into .venv/lib/..., completely isolated from every other project

Leaving the environment is just deactivate — no arguments needed:

Bash
deactivate

.venv/ should never be committed to version control — it's a local build artifact, entirely reproducible from the dependency list described below, and it's usually large and platform-specific. Add it to .gitignore the moment you create it.

pip and requirements.txt

pip is Python's standard package installer, pulling from the Python Package Index (PyPI) by default:

Bash
pip install requests
pip install "django>=5.0,<6.0"     # a version constraint
pip uninstall requests
pip list                            # everything installed in the active environment

A project records its dependencies in a requirements.txt file so anyone else (or a deployment pipeline) can recreate the exact same set of installed packages:

Plaintext
requests==2.32.3
django>=5.0,<6.0
python-dotenv==1.0.1

Generate one from what's currently installed in the active environment:

Bash
pip freeze > requirements.txt

And install everything a project needs from someone else's requirements.txt — the standard first step after cloning a Python repository:

Bash
python3 -m venv .venv
source .venv/bin/activate
pip install -r requirements.txt

Pinning exact versions (==2.32.3 rather than a loose >=2.32) makes builds reproducible — the same requirements.txt installs the identical set of packages today and a year from now, rather than silently picking up whatever the latest compatible release happens to be at install time.

A complete project setup, start to finish

Bash
mkdir my-project && cd my-project
python3 -m venv .venv
source .venv/bin/activate

pip install requests python-dotenv
pip freeze > requirements.txt

echo ".venv/" >> .gitignore
git init
git add .
git commit -m "Initial project setup"

Anyone else cloning this repository runs exactly three commands to get an identical, working environment: create a venv, activate it, pip install -r requirements.txt.

Modern alternatives: uv and poetry

venv + pip is the reliable, zero-install baseline every Python developer should know — but two newer tools have become popular for the same job, adding dependency resolution, lockfiles, and much faster installs:

  • uv — an extremely fast (written in Rust) drop-in replacement for pip and venv, from the makers of Ruff. uv venv creates an environment, uv pip install -r requirements.txt installs from a requirements file with dramatically better performance than pip, and uv add requests manages a project's dependencies with an automatic lockfile (uv.lock) for fully reproducible installs. It's rapidly becoming a common default for new projects because it's a near drop-in replacement with far less waiting.
  • poetry — an older, more opinionated all-in-one tool that manages dependencies, virtual environments, and packaging (building a distributable package for PyPI) through a single pyproject.toml file and a poetry.lock lockfile. poetry add requests records the dependency and updates the lockfile; poetry install reproduces the exact locked environment on another machine.

Both solve the same core problem as venv/pip/requirements.txt — reproducible, isolated dependencies — with an automatic lockfile that pins the entire dependency tree (including transitive dependencies) exactly, rather than relying on a hand-maintained requirements.txt. venv/pip remains completely fine for small scripts and is worth understanding first, since it's what's always available with zero setup — but don't be surprised to see uv or poetry as the standard in a professional codebase.

Comparing the options

Tool Needs installing Lockfile Speed Typical use
venv + pip No — built into Python No (manual requirements.txt) Baseline Learning, small scripts, maximum compatibility
uv Yes (one binary) Yes (uv.lock) Very fast New projects wanting speed with minimal ceremony
poetry Yes (one binary) Yes (poetry.lock) Moderate Projects that also need to build/publish a package to PyPI

Common mistakes

  • Installing packages globally (no virtual environment activated at all) — it works until a second project needs a conflicting version, and by then dependencies from several unrelated projects are tangled together in one global site-packages.
  • Committing .venv/ (or venv/) to version control — it's large, platform-specific, and entirely reproducible from requirements.txt, so it belongs in .gitignore, not in the repository.
  • Forgetting to activate the virtual environment before installing a package, silently installing it globally instead — always confirm with which python (or check the shell prompt) that the environment is active first.
  • Using unpinned versions (requests with no version at all) in a requirements.txt meant for reproducible deployments — a fresh install months later can pull a newer, possibly breaking release.

Interview questions

Q: What problem does a virtual environment actually solve? It isolates a project's installed packages (and their versions) from every other project's and from the system-wide Python installation, so two projects on the same machine can depend on different, even conflicting, versions of the same package without interfering with each other.

Q: What's the difference between pip freeze output and a hand-written requirements.txt? pip freeze lists the exact versions of every package currently installed in the active environment (including transitive dependencies pulled in automatically), which is why it's typically redirected straight into requirements.txt to snapshot a known-working set. A hand-written file is whatever the author chose to list, possibly with loose version ranges instead of pins — less reproducible, but sometimes intentional when a library wants to declare compatibility with a range of versions rather than one exact release.