Skip to main content

📦 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.txt and 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."

flowchart TD Dev([You]) --> A[Project A
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.
Isolated virtual environments on one machine One computer system contains a shared system Python plus two separate virtual environments, each with its own packages and versions. Your Computer System Python /usr/bin/python (leave it alone) numpy 1.19 requests 2.25 venv: project_a .venv/bin/python django 4.2.0 psycopg2 2.9 requests 2.31 venv: project_b .venv/bin/python flask 3.0.0 sqlalchemy 2.0 requests 2.26
Figure 1 — Each environment carries its own interpreter and packages. Two of them can hold different versions of 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:

  1. Edits PATH: it prepends the environment's bin (or Scripts on Windows) directory, so python resolves to the environment's interpreter first.
  2. Changes the prompt: it adds a (.venv) prefix so you can see which environment is active.
  3. 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.

sequenceDiagram participant User participant Shell participant VEnv as .venv participant Sys as System Python User->>Shell: source .venv/bin/activate Shell->>VEnv: run activate script VEnv->>Shell: prepend .venv/bin to PATH VEnv->>Shell: set prompt & VIRTUAL_ENV User->>Shell: pip install flask Shell->>VEnv: installs into .venv/site-packages User->>Shell: deactivate VEnv->>Shell: restore original PATH & prompt User->>Shell: python --version Shell->>Sys: resolves to system Python again

📖 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.

ToolStrengthsBest for
venvBuilt into Python, simple, no installAlmost everyone — the default
virtualenvFaster creation, works with older Pythons, more optionsLegacy support, specific interpreter versions
condaManages non-Python (C/CUDA) binaries tooData science / scientific stacks
PoetryEnv + dependency resolution + lock file + packagingApplications and libraries wanting reproducible builds
uvExtremely fast (Rust-based); creates envs & installsModern 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:

  • Poetrypyproject.toml (what you want) + poetry.lock (exactly what's installed).
  • uv / pip-tools — compile a requirements.in into a fully-pinned requirements.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

  1. Make a working folder and enter it.
  2. Create env_old and install an older requests.
  3. Create env_new and install a newer requests.
  4. Activate each in turn and print the installed version to prove they differ.
  5. 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 .venv and 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 RemoteSigned for 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 .venv then activate is the built-in, universal workflow.
  • Activation just edits your shell's PATH, prompt, and VIRTUAL_ENV — reversible with deactivate.
  • 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

🚀 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.