📦 Python Virtual Environments
Two projects on your machine want two different versions of Django. Install one system-wide and you break the other. Virtual environments end that "dependency hell" by giving each project its own private, disposable Python — the single most important habit in professional Python development.
🎯 Learning Objectives
By the end of this lesson, you will be able to:
- Explain the dependency-conflict problem virtual environments solve
- Create, activate, and deactivate an environment with the built-in
venv - Describe what activation actually does to your shell (PATH, prompt, interpreter)
- Compare venv, virtualenv, conda, Poetry, and uv and pick the right one
- Capture and restore dependencies with
requirements.txtand lock files
Estimated Time: 30–40 minutes • Difficulty: Beginner
Hands-on: Create two isolated environments with conflicting package versions and prove they don't interfere.
In This Lesson
The Dependency Problem
Suppose Project A needs Django 3.2 and Project B needs Django 4.2. If you pip install Django globally, only one version can exist at a time — installing one silently breaks the other. Scale that to dozens of packages across many projects and you get "dependency hell."
Django 3.2] Dev --> B[Project B
Django 4.2] Dev --> C[Project C
NumPy 1.18] A -.conflict.-> Sys[(System-wide Python
one version per package)] B -.conflict.-> Sys C -.conflict.-> Sys
There's a second, sharper reason to avoid installing into the system Python: on Linux and macOS the operating system itself depends on that Python. Modern Python even blocks global installs with an "externally-managed-environment" error to stop you from breaking your OS. Virtual environments are the sanctioned way out.
What Is a Virtual Environment?
A virtual environment is a self-contained directory holding a specific Python interpreter and its own independent set of installed packages, isolated from other environments and from the system Python.
💡 Analogy: Think of each project getting its own kitchen. Without virtual environments, every chef shares one kitchen and fights over the same stove and ingredients. With them, each chef gets a private kitchen stocked with exactly what their recipe needs — no conflicts, no compromises.
requests with zero conflict.✅ Why it's worth the habit
- Isolation: each project's dependencies are separate.
- Reproducibility: record exactly what's installed so anyone can recreate it.
- No admin rights needed: install packages without touching system Python.
- Clean deletion: a broken environment is just a folder — delete it and rebuild.
Using venv (Built-in)
Since Python 3.3, the venv module ships with Python — no installation required. It's the right default for most projects. The convention is to name the folder .venv so editors like VS Code detect it automatically.
# Create an environment in a folder named .venv
python -m venv .venv
# Activate it — macOS / Linux
source .venv/bin/activate
# Activate it — Windows (PowerShell)
.venv\Scripts\Activate.ps1
# Activate it — Windows (cmd.exe)
.venv\Scripts\activate.bat
# Your prompt now shows (.venv). Install packages into it:
pip install flask
# When you're done working in this project:
deactivate
📖 Once activated, commands are local
With the environment active, python and pip refer to the copies inside .venv. Anything you pip install lands in that environment's site-packages and nowhere else. To be certain which interpreter you're using, run python -c "import sys; print(sys.executable)".
How Activation Works
"Activation" sounds magical, but it's simple. The activate script does three things to your current shell:
- Edits PATH: it prepends the environment's
bin(orScriptson Windows) directory, sopythonresolves to the environment's interpreter first. - Changes the prompt: it adds a
(.venv)prefix so you can see which environment is active. - Sets
VIRTUAL_ENV: an environment variable tools use to detect the active environment.
Nothing is permanently installed by activation, and it only affects the current terminal. deactivate simply restores the original PATH and prompt.
📖 The folder inside
.venv/
├── bin/ # Scripts/ on Windows
│ ├── activate # the activation script
│ ├── python # this env's interpreter (symlink/copy)
│ └── pip # this env's pip
├── lib/pythonX.Y/site-packages/ # installed packages live here
└── pyvenv.cfg # points back to the base Python
Because it's just a folder, you never commit it to Git — you recreate it from your recorded dependencies instead (next section).
The Tool Landscape
Several tools create environments. Here's how the main ones compare — and when to reach for each.
| Tool | Strengths | Best for |
|---|---|---|
| venv | Built into Python, simple, no install | Almost everyone — the default |
| virtualenv | Faster creation, works with older Pythons, more options | Legacy support, specific interpreter versions |
| conda | Manages non-Python (C/CUDA) binaries too | Data science / scientific stacks |
| Poetry | Env + dependency resolution + lock file + packaging | Applications and libraries wanting reproducible builds |
| uv | Extremely fast (Rust-based); creates envs & installs | Modern projects wanting speed; a drop-in for pip/venv |
💡 What's changed recently
uv (from the makers of the Ruff linter) has quickly become popular: it creates environments and installs packages far faster than pip, and can replace venv + pip in one tool. uv venv then uv pip install ... mirrors the classic workflow. Meanwhile pipenv — once the trendy choice — has faded in favor of Poetry and uv. When in doubt as a beginner, start with plain venv; it's universal and teaches you the fundamentals.
The same task in three tools
# venv (built-in)
python -m venv .venv && source .venv/bin/activate
# conda
conda create --name myenv python=3.12
conda activate myenv
# uv (fast, modern)
uv venv # creates .venv
source .venv/bin/activate
uv pip install flask
Sharing Dependencies
A teammate can't use your environment folder — but they can rebuild an identical one from a recorded list. The classic mechanism is requirements.txt.
# Record everything currently installed (exact versions)
pip freeze > requirements.txt
# On another machine, recreate the environment
python -m venv .venv
source .venv/bin/activate
pip install -r requirements.txt
⚠️ The trouble with plain pip freeze
pip freeze dumps every package including indirect dependencies, so you can't tell what you actually asked for versus what came along for the ride. A cleaner pattern for applications is to hand-maintain your direct dependencies with sensible ranges:
# requirements.txt — direct dependencies only
Django>=4.2,<5.0
requests>=2.28.0
python-dotenv>=1.0.0
Lock files: the modern answer
For fully reproducible builds you want a lock file that pins exact versions (and hashes) of the entire dependency tree, separate from your high-level requirements:
- Poetry —
pyproject.toml(what you want) +poetry.lock(exactly what's installed). - uv / pip-tools — compile a
requirements.ininto a fully-pinnedrequirements.txt.
# pyproject.toml (Poetry excerpt)
[tool.poetry.dependencies]
python = "^3.12"
django = "^4.2"
requests = "^2.28.0"
[tool.poetry.group.dev.dependencies]
pytest = "^7.3"
black = "^23.3"
📖 Environments isolate packages, not secrets
A virtual environment does not manage configuration like database URLs or API keys. Keep those in a .env file (git-ignored) and load them with python-dotenv:
import os
from dotenv import load_dotenv
load_dotenv() # reads .env into os.environ
DEBUG = os.getenv("DEBUG", "False").lower() == "true"
DB_URL = os.getenv("DATABASE_URL")
Hands-on Exercise
🏋️ Prove isolation with conflicting versions
Objective: Create two environments that install different versions of the same package and confirm they don't affect each other.
Instructions
- Make a working folder and enter it.
- Create
env_oldand install an olderrequests. - Create
env_newand install a newerrequests. - Activate each in turn and print the installed version to prove they differ.
- Export each environment's dependencies to its own requirements file.
mkdir venv_lab && cd venv_lab
# Environment 1 — older requests
python -m venv env_old
source env_old/bin/activate # Windows: env_old\Scripts\activate
pip install "requests==2.25.1"
python -c "import requests; print('env_old:', requests.__version__)"
pip freeze > requirements-old.txt
deactivate
# Environment 2 — newer requests
python -m venv env_new
source env_new/bin/activate
pip install "requests==2.31.0"
python -c "import requests; print('env_new:', requests.__version__)"
pip freeze > requirements-new.txt
deactivate
💡 Hint
If activation fails in Windows PowerShell with an execution-policy error, run Set-ExecutionPolicy -ExecutionPolicy RemoteSigned -Scope CurrentUser once, then try again. On macOS/Linux use source env_old/bin/activate.
✅ Expected result
env_old prints 2.25.1 and env_new prints 2.31.0. Two versions of the same library coexist on one machine with zero conflict — that's the whole point. Comparing the two requirements files also shows how each environment captured its own dependency tree.
🎯 Quick Quiz
Question 1: What does activating a virtual environment actually change?
Question 2: Which command creates a virtual environment using only the standard library?
Question 3: Why do lock files (poetry.lock, a compiled requirements.txt) improve on a hand-written requirements list?
Best Practices & Troubleshooting
✅ Do
- Name the folder
.venvand keep it inside the project, added to.gitignore. - Create a fresh environment per project, never reuse one across unrelated projects.
- Commit your requirements/lock file so teammates and CI rebuild the same environment.
- Document setup in your README: create env → activate → install → run.
⚠️ Common issues
- Wrong interpreter? Confirm with
python -c "import sys; print(sys.executable)"— the path should point inside.venv. - PowerShell won't activate? Set the execution policy to
RemoteSignedfor the current user (see hint above). - Environment broken or corrupted? It's disposable:
deactivate, delete the folder, recreate it, and reinstall from your requirements file. - "externally-managed-environment" error? You're trying to install into system Python — create and activate a venv first.
Summary & Quiz
🎉 Key Takeaways
- Virtual environments give each project its own isolated interpreter and packages, ending version conflicts.
python -m venv .venvthen activate is the built-in, universal workflow.- Activation just edits your shell's PATH, prompt, and
VIRTUAL_ENV— reversible withdeactivate. - venv is the default; conda suits data science; Poetry and uv add resolution, lock files, and speed.
- Never commit the env folder — commit a requirements or lock file and rebuild from it.
📚 Further Reading
- Python venv Documentation
- uv Documentation
- Poetry Documentation
- Real Python — Virtual Environments Primer
🚀 What's Next?
Now that you have a clean place to install things, let's master the tool that installs them. Next: Python Package Management with pip — PyPI, version specifiers, requirements files, and security.
🎉 Great work!
Isolated environments are the foundation every professional Python project stands on. You've got it.