Virtual environments, pip, and packaging basics
Python installs packages globally by default, which means two projects on the same machine needing different versions of the same library genuinely can't both be satisfied at once — a virtual environment exists specifically to give each project its own isolated set of installed packages.
4 min read
The problem: one global site-packages, shared by every project
Without a virtual environment, `pip install requests` installs into the
SAME global Python installation every project on the machine uses —
project A needing requests==2.28 and project B needing requests==2.31
genuinely cannot both be satisfied at once, since there's only one
global copy of "requests" installed at a time
By default, pip install places a package into Python's global site-packages directory — shared across every script and project run with that same Python installation. Two projects needing different, incompatible versions of the same library is a genuinely common real situation, and a single global install can't satisfy both simultaneously — installing one project's required version silently breaks the other project's requirement, without any error at the moment of installation.
A virtual environment: an isolated, project-specific copy of installed packages
python -m venv .venv # creates a new, isolated environment in the .venv folder
source .venv/bin/activate # macOS/Linux — activates it for the current shell session
.venv\Scripts\activate # Windows
pip install requests # installs into THIS environment only, not the global onepython -m venv .venv creates a self-contained directory with its own Python interpreter (really a lightweight reference to the real one) and its own, empty site-packages — activating it changes the current shell's python/pip commands to point at this isolated environment instead of the global installation, so anything installed afterward is scoped to just this project. This is the direct, structural fix for the version-conflict problem: each project gets its own environment, and two projects' virtual environments can hold two completely different versions of the same package without any conflict at all.
requirements.txt: recording exactly what a project needs, for reproducibility
# requirements.txt
requests==2.31.0
flask==3.0.0
pip freeze > requirements.txt # captures the EXACT versions currently installed
pip install -r requirements.txt # installs those exact versions elsewhere — a new machine, a teammate, CIrequirements.txt is a plain text manifest of a project's dependencies, and pip freeze generates it from whatever's actually installed in the current (activated) environment — pinning exact versions (==2.31.0, not a loose range) is the common convention specifically because it makes installs reproducible: anyone running pip install -r requirements.txt gets the identical set of package versions the original environment had, the same reproducibility goal this platform's Node.js domain covered for package-lock.json, just achieved through a plain committed text file instead of an automatically-generated lockfile.
pyproject.toml: the modern manifest, beyond just dependencies
[project]
name = "my-app"
version = "1.0.0"
dependencies = ["requests>=2.28", "flask>=3.0"]
[project.optional-dependencies]
dev = ["pytest>=7.0", "mypy>=1.0"]pyproject.toml is the modern, standardized manifest format (defined by real Python packaging specs, not tool-specific) for a project's metadata, dependencies, and build configuration — covering more than requirements.txt's plain dependency list, including the project's own name/version (needed if the project itself will be packaged and distributed) and separate optional dependency groups (like dev, for tools only needed during development, mirroring the dependencies/devDependencies split this platform's Node.js domain covered for package.json). Modern Python tooling (pip, poetry, uv) increasingly reads this file as the primary source of truth, with requirements.txt remaining common for simpler projects or as a generated/exported artifact.
Why activating the right environment matters more than it might seem
# A genuinely common real mistake: installing into the WRONG environment,
# then being confused why "pip install X" didn't fix an ImportError —
# because X was installed globally, but the script is actually being run
# through a virtual environment that never had it installed at allForgetting to activate a project's virtual environment before running pip install (installing into the global environment or a different project's environment by accident) is a genuinely common, real source of "I installed it, but Python still says it's not found" confusion — the fix is always checking which environment is actually active (which python on macOS/Linux, or checking the shell prompt, which most virtual environment activation scripts prefix) before assuming an install went where it was intended to.
Further reading
Check your understanding
A quick comprehension check — not tracked, not graded, just for you.
1. Why can't two projects on the same machine both have their dependencies satisfied using only the global Python installation?
2. What does `python -m venv .venv` actually create?
3. What's the practical purpose of pinning exact versions in requirements.txt (like requests==2.31.0) rather than a loose range?