You clone a coworker's project, run pip install -r requirements.txt, and everything looks fine. Two days later a completely different project on your machine starts throwing import errors. Nothing about that project changed. You didn't touch it. And yet it's broken.

That's the moment most people meet Python's dependency problem — not by reading about it, but by being bitten by it. One global pile of packages, every project reaching into the same pile, and the last install quietly winning every argument.

The fix is old and well understood: give every project its own isolated set of packages. What has changed is how you do it. The tooling in 2026 is faster and less fiddly than the ritual most of us learned years ago, and it's worth updating your muscle memory.

A virtual environment isn't a Python feature you should have to think about. It's a boundary that keeps yesterday's project from breaking today's.

What actually goes wrong without isolation

Python installs packages into a shared site-packages directory tied to your interpreter. Install requests once and every script on the machine can import it. That sounds convenient right up until two projects want different versions of the same library.

Say project A pins a data library at version 1.4 because a function signature changed in 2.0. Project B needs 2.1 for a bug fix. Install B's requirements and you've silently downgraded — sorry, upgraded — A out from under itself. Nothing warns you. The failure shows up later as a TypeError on a line you never edited, and you spend forty minutes suspecting your own code.

There's a second, quieter cost: you can't reproduce your own environment. If your machine accumulates 200 packages over three years, and your project genuinely needs nine of them, you have no reliable way to tell a teammate — or a production server — what to install. Everything works locally because your laptop is a junk drawer that happens to contain the right junk.

Isolation solves both. One directory per project, containing exactly the packages that project declared. Delete the directory, recreate it from a file, and you're back where you started.

The built-in way: venv

Python has shipped venv in the standard library since 3.3. There's nothing to install, and on a locked-down machine where you can't add tooling, this is your answer.

# create an environment in the project folder
python3 -m venv .venv

# activate it (macOS / Linux)
source .venv/bin/activate

# activate it (Windows PowerShell)
.venv\Scripts\Activate.ps1

# now pip installs land inside .venv, not system-wide
pip install requests
pip freeze > requirements.txt

Two conventions matter more than they look. Name the folder .venv. Editors, linters, formatters and virtually every .gitignore template already recognize that exact name, so tooling finds your interpreter without configuration. And never commit the folder — it contains compiled binaries specific to your OS and CPU. Commit the list, not the contents.

The friction with venv is the activation dance. Open a new terminal tab, forget to activate, run your script, get a confusing ModuleNotFoundError. Multiply that by every context switch in a day. It's not hard, it's just a small tax you pay forever.

The 2026 default: uv

uv is a Rust-based tool from Astral that replaces pip, venv, and a few others in one binary. The headline is speed — dependency resolution and installs that are commonly reported as 10–100× faster than pip. On a project with fifty dependencies that's the difference between "go get coffee" and "already done."

But the speed isn't actually the best part. The best part is that it removes the activation step entirely:

# start a project (creates pyproject.toml)
uv init my-app
cd my-app

# add a dependency — creates .venv and uv.lock automatically
uv add requests

# run anything inside the environment, no activation needed
uv run python main.py
uv run pytest

uv run resolves the environment for you every time. Open five terminal tabs, never activate anything, and every command still uses the right interpreter with the right packages. If you've ever debugged an import error that turned out to be "wrong terminal," you'll feel the difference within a week.

One rule if you adopt it: use uv add, not pip install, inside a uv project. uv add updates both pyproject.toml and the lock file. Reaching for pip installs the package but leaves your declared dependencies out of sync — which is exactly the reproducibility problem you were trying to escape.

Requirements files versus lock files

These two get conflated constantly, and the distinction is the whole reason lock files exist.

A requirements.txt produced by pip freeze is a snapshot of whatever happened to be installed on your machine at that moment. It's flat, it doesn't distinguish "things I asked for" from "things those things dragged in," and it doesn't record why any particular version was chosen.

A lock file (uv.lock, or poetry.lock) records the fully resolved dependency graph — every package, every exact version, with hashes. Recreate from it on another machine and you get a byte-identical set of packages, not "something that resolves today."

requirements.txtLock file
What it recordsInstalled package listFull resolved graph + hashes
Reproducible across machinesUsuallyYes, exactly
Separates direct vs. transitive depsNoYes
Best forSimple scripts, legacy CIApplications, teams, deployment

The practical rule: applications should commit a lock file; libraries should declare loose version ranges and let the consuming application do the locking. A library that pins requests==2.31.4 is a library nobody can install alongside anything else.

Choosing without overthinking it

Most of the "which tool" debate dissolves once you name the actual situation you're in.

A one-file script with no dependencies needs no environment at all. Don't create ceremony for a thirty-line utility.

A quick script with two or three packages, on a machine where you can't install tooling — use venv. It's there, it works, and the whole setup is two commands.

A new application or service you'll maintain — start with uv. Lock file by default, no activation, fast enough that recreating the environment from scratch stops feeling like a punishment.

Heavy data science with compiled scientific stacksconda still earns its place, because it manages non-Python binaries (BLAS, CUDA toolkits, GDAL) that pip-based tools historically struggle with.

Command-line tools you want available everywhere, like a formatter or a linter, don't belong in any project environment. Install them in their own isolated space with uv tool install or pipx so they never collide with project dependencies.

Notice that none of these branches are about taste. They're about what the project actually is.

Making it stick

Habits beat knowledge here. Three that pay for themselves:

Add .venv/ to your global gitignore once, not per project. You'll never again review a pull request containing four thousand binary files.

Delete and rebuild your environment occasionally — monthly is plenty. If rm -rf .venv && uv sync produces a working project, your declared dependencies are honest. If it doesn't, you've just found a package you were relying on by accident, and better to find it now than in a deploy.

Put the setup commands in your README, in the exact form a newcomer should paste. Two lines. It's the single highest-return documentation you can write, and it forces you to verify the project starts from nothing.

The short version

Global package installs are a shared mutable state problem wearing a friendly face — they work until two projects disagree, and then they fail in ways that don't point at the cause. Every project gets its own environment, named .venv, never committed.

Reach for venv when you need zero installation or you're keeping something old alive. Reach for uv for anything new: it's dramatically faster, it locks by default, and it quietly deletes the "did I activate?" question from your day. Commit the lock file, not the folder. Rebuild from scratch now and then to prove you can.

None of this is glamorous work. But the version of you who clones this repo eighteen months from now — on a new laptop, under deadline, remembering none of it — will be very glad you spent the ten minutes.