Project A was an old scraper, pinned to an older requests. Project B was new and needed the current one. Without thinking, I ran the upgrade for B — and the next time I opened A, it died on import with the error I can still recite: ImportError: cannot import name 'url_quote' from 'requests.compat'.
Nothing was wrong with either project. The problem was that on my machine there was one Python and one site-packages folder, shared by everything I'd ever installed. Upgrading a package for one project silently changed it for all the others. A virtual environment (venv) fixes exactly this: it gives each project its own private copy of Python's installed packages.
Inside the project folder:
python -m venv .venv
That makes a folder named .venv holding a private Python — venv ships with Python itself, so there's nothing to install first. Then you activate it, so your shell uses that Python instead of the system one:
.venv\Scripts\activate # Windows
source .venv/bin/activate # macOS / Linux
Your prompt gains a (.venv) prefix, and from this moment pip install goes into the project's sandbox and nowhere else. Deactivate with deactivate when you're done. The classic symptom of a forgotten activation: pip install succeeds, then import fails with ModuleNotFoundError: No module named 'requests' — the package went into whatever environment your shell was actually pointing at. Check the prompt for the (.venv) prefix before anything else. The one mental model worth keeping: the venv is just a folder — activating only points your shell's python at it. Nothing was installed into Python itself, and nothing about your system changed.
While the venv is active, freeze what you've installed:
pip freeze > requirements.txt
That file is the project's shopping list — every package with its exact version. On a new machine, or a year later when the laptop dies, rebuilding the environment is three commands: create the venv, activate it, and pip install -r requirements.txt. Commit requirements.txt; never commit .venv itself, which is large, machine-specific, and one python -m venv .venv away from existing again.
My favorite property of venvs: if one goes bad — a broken upgrade, an experimental install, the scraper situation above — you delete the folder. That's it. The entire environment is that one directory; removing it removes the mess, touching nothing else on the system. Recreate it from requirements.txt in under a minute. I've reset a venv more times than I've debugged one, and it's faster every time.
Always create it inside the project folder, always named .venv, so six months from now it's obvious which environment belongs to which project — and so the two-projects-breaking-each-other story stays a story you tell, not one you live again.