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):
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:
# 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:
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:
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:
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:
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:
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:
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
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 forpipandvenv, from the makers of Ruff.uv venvcreates an environment,uv pip install -r requirements.txtinstalls from a requirements file with dramatically better performance thanpip, anduv add requestsmanages 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 singlepyproject.tomlfile and apoetry.locklockfile.poetry add requestsrecords the dependency and updates the lockfile;poetry installreproduces 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/(orvenv/) to version control — it's large, platform-specific, and entirely reproducible fromrequirements.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 (
requestswith no version at all) in arequirements.txtmeant 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.