Virtual environments and pip, explained
Most "it works on my machine" problems in Python trace back to one thing: packages installed globally, shared by every project, at whatever version happened to be current the day you installed them. Virtual environments solve it in about thirty seconds, and understanding why makes the commands stick.
The problem they solve
Install a package with pip install requests and, without an environment active, it lands in your system Python. Now every project on the machine sees it. That's fine until project A needs Django 4 and project B needs Django 5 — one global install can't be both, so one of the projects breaks.
A virtual environment is a folder containing its own python and its own site-packages. Activating it changes which interpreter your shell reaches for, so installs go into the project instead of the machine.
Creating and activating one
python3 -m venv .venv-m venv runs the standard library's venv module — nothing to install first. The .venv name is a convention that editors like VS Code detect automatically.
Activation differs by shell:
source .venv/bin/activate.venv\Scripts\Activate.ps1Your prompt gains a (.venv) prefix. Verify it's really in effect:
which python
python -c "import sys; print(sys.prefix)"Both should point inside your project folder. When you're done, deactivate returns the shell to normal.
An environment is active only in the shell that activated it. New terminal tab, new SSH session, or a fresh VS Code terminal means activating again. If pip install suddenly needs sudo, that's the tell — you're on the system Python and about to install globally.
What pip actually does
pip install requests resolves the name against PyPI, downloads the package plus everything it depends on, and unpacks it into the active environment's site-packages. That last part matters: with no environment active, "the active environment" is your entire system.
pip install requests
pip list
pip show requestspip list shows what's installed — note it includes packages you never asked for, because dependencies bring their own dependencies. pip show gives the version, location, and what requires it.
Recording dependencies
Your environment folder should never be committed to git; the record of what goes in it should be. The traditional approach:
pip freeze > requirements.txtThat writes every installed package with an exact version, and someone else recreates it with:
pip install -r requirements.txtThe catch: pip freeze doesn't distinguish what you asked for from what came along as a dependency. Six months later nobody can tell whether urllib3 is something the project uses directly or just something requests dragged in.
The modern alternative is pyproject.toml, where you list only your direct dependencies:
[project]
name = "expense-tracker"
version = "0.1.0"
dependencies = [
"requests>=2.31",
"rich>=13.0",
]Then install the project itself, and pip resolves the rest:
pip install -e .-e installs in editable mode — a link to your source rather than a copy, so your edits take effect without reinstalling.
Version specifiers
requests with no version means "whatever is newest today", which is how a project that worked in January fails to install in June. The common specifiers:
requests==2.31.0— exactly this versionrequests>=2.31— this or newerrequests~=2.31.0— 2.31.x, but not 2.32
~= is usually the right default for libraries that follow semantic versioning: you get patch fixes automatically and never a surprise major upgrade.
Add it to .gitignore
.venv/
__pycache__/
*.pycThe environment is thousands of files, is platform-specific, and is fully reproducible from requirements.txt or pyproject.toml. Committing it bloats the repository and breaks for anyone on a different OS.
The whole workflow
mkdir myproject && cd myproject
python3 -m venv .venv
source .venv/bin/activate
pip install requests
pip freeze > requirements.txtFive commands, and the project is isolated from every other one on your machine. Do it before the first pip install, not after — untangling globally installed packages later is far more work than starting clean.
Faster alternatives to venv and pip
venv and pip are the standard library tools and work everywhere with no install — everything above applies regardless of what a specific project uses day to day. A newer generation of tools (uv is the most prominent as of this writing) reimplements the same environment-and-install workflow significantly faster, often as a drop-in replacement for the pip commands above:
uv venv
uv pip install requestsNEEDS CURRENT VERSION VERIFICATION: tooling in this space moves quickly enough that a specific recommendation risks going stale — check a tool's own current documentation before adopting it for a new project, rather than treating any single tool as a permanent default. The underlying concepts on this page (isolated environments, pinned dependencies, editable installs) apply the same way regardless of which specific tool manages them.
FAQ
Do I need to reactivate the environment every time I open a new terminal? Yes — activation only affects the shell session it was run in, exactly like the callout above on per-terminal activation. This is the most common point of confusion for anyone new to virtual environments.
What happens if I pip install something without activating the environment first?
It installs into whichever Python the shell currently resolves to — usually the system Python, not your project's isolated one. Running which python (or checking sys.prefix, as shown above) before installing is the reliable way to confirm which one is actually active.
Can two projects share one virtual environment?
Technically yes, but it defeats the actual purpose — the moment two projects need different versions of the same package, a shared environment reintroduces the exact conflict venv exists to prevent. One environment per project is the pattern worth keeping.
Is requirements.txt deprecated now that pyproject.toml exists?
No — requirements.txt remains widely supported and simple for straightforward cases. pyproject.toml is the more modern, more precise choice for a project with a real distinction between direct and transitive dependencies, but neither is strictly required over the other for every project.