Python Virtual Environments 101
Why Python projects need their own environment, how to create one with venv, pipenv or uv, and the habits that keep dependencies reproducible.

Python Virtual Environments 101
Sooner or later everyone hits the same wall with Python. One project needs a library at version 2.x, another one breaks on anything newer than 1.x, and both of them are installed into the same global site-packages. Whatever you upgrade, something else stops working. A virtual environment is the fix, and it is a two-command habit once you have the pattern.
What a virtual environment actually is
A virtual environment is a directory that holds its own Python interpreter link and its own site-packages. When it is active,
pip install writes into that directory instead of the system Python, and import resolves packages from there first. Nothing outside the directory changes.
That gives you three things that matter:
- Dependencies are isolated per project, so installing something for one app cannot break another.
- Different projects can pin different versions of the same package. A project stuck on Django 4.2 LTS and a new one on 5.2 can live side by side on the same machine.
- Setups become reproducible. Whoever clones the repo creates their own environment from the same list of pinned versions and gets the same result you have.
Creating one with venv
venv ships with Python, so there is nothing to install. From the project directory:
cd my-project python3 -m venv .venv source .venv/bin/activate # macOS / Linux .venv\Scripts\activate # Windows (cmd)
The prompt picks up the environment name once it is active. From there, install what the project needs and freeze it:
python -m pip install -r requirements.txt python -m pip freeze > requirements.txt
Two details that trip people up. First,
python -m pip is safer than a bare pip, because it guarantees you are installing into the interpreter you activated. Second, from Python 3.12 onwards a new environment does not include setuptools by default, so a package with a legacy build step may need python -m pip install setuptools first.
When you are done,
deactivate returns you to the system Python. Add .venv/ to .gitignore and commit requirements.txt instead; environments are local, version pins are shared.
Pipenv
pipenv combines the environment and the dependency file into one tool. In a project directory:
pipenv install
That creates the virtual environment and a
Pipfile if neither exists. The Pipfile has two sections: [packages] for runtime dependencies and [dev] for things like test runners and linters. Install a package and it gets added there:
pipenv install requests==2.32.3 pipenv install --dev pytest
pipenv install after that reads the Pipfile, installs the missing pieces and writes Pipfile.lock, which pins every transitive dependency so two machines end up identical. pipenv shell activates the environment, and exit leaves it. (pipenv exit is not a command, which is the kind of thing you only find out when it hangs the first time.)
Pipenv still works and plenty of projects use it. It is no longer the default recommendation from the packaging side, which has settled back on plain
venv plus a locking tool. If you are starting a new project today, venv plus uv or pip-tools is the lighter path.
The fast option: uv
uv is a resolver and installer written in Rust that most Python developers I know have switched to over the last year, largely because it is an order of magnitude faster and handles the environment for you:
uv venv # create .venv
source .venv/bin/activate
uv pip install -r requirements.txt
# or manage a project without activating anything:
uv init
uv add requests
uv run python main.py
uv run is the part I like: it uses the project environment without you remembering to activate it first, so a scheduled script or a cron job behaves the same as your terminal.
The rules I keep
- One environment per project. Never install project packages into the system Python if you can avoid it.
- Commit the pinned list (
,requirements.txt
,Pipfile.lock
) and ignore the environment directory.uv.lock - Pin versions for anything that goes to production. Floating versions are how a build that worked on Friday fails on Monday.
- When an environment gets strange, delete
and recreate it. It takes seconds and saves an afternoon of debugging the environment itself..venv
If you want to see the payoff, the RAG agent walkthrough and the Titanic machine learning tutorial both pull in a handful of data and ML packages that would be a mess to install globally. Same story with running ComfyUI on a rented GPU, where the app expects its own Python with its own dependency set.
