Python Virtual Environments with venv and uv
Isolate your Python projects the right way — using the built-in venv module, the new uv tool, and a clear mental model for when to use pipx instead.
What you'll learn
- ✓Why per-project isolation matters
- ✓How to create and activate a venv
- ✓How to pin dependencies with requirements.txt
- ✓What uv is and why it is fast
- ✓When to reach for pipx instead
Prerequisites
- •A working Python 3.10+ install — see Functions in Python if you are new to the language
Every Python project has dependencies, and every dependency has a version. The moment you install one library globally, you have started a race against your future self — a race where the prize is “this project still runs in six months.” Virtual environments stop the race before it begins. This post covers the built-in venv module, the much faster uv tool, and the boundary between them and pipx.
Why isolate at all
When you pip install requests, by default it lands in your system Python’s site-packages. That is the same directory every other project on the machine uses. If project A wants requests==2.28 and project B wants requests==2.32, you cannot have both. Worse, an OS update can wipe or replace the system Python and break everything.
A virtual environment is just a directory containing:
- A copy (or symlink) of the Python interpreter
- Its own
site-packagesdirectory - A small
pyvenv.cfgfile
When activated, it puts its own bin/ (or Scripts/ on Windows) at the front of your PATH. python and pip now refer to the environment’s copies. Nothing else on the system is affected.
Creating a venv
The venv module ships with Python. From inside your project folder:
# Create a directory called .venv with a fresh interpreter
python -m venv .venv
The name .venv is conventional — editors like VS Code and PyCharm look for it automatically, and the leading dot keeps it out of casual file listings.
Activating and deactivating
Activation is shell-specific:
# macOS / Linux (bash, zsh)
source .venv/bin/activate
# Windows (PowerShell)
.venv\Scripts\Activate.ps1
# Windows (cmd)
.venv\Scripts\activate.bat
Your prompt usually gains a (.venv) prefix. Check that python now points inside the project:
which python # .../my-project/.venv/bin/python
python -V # Python 3.12.4
To leave the environment:
deactivate
This is just a shell function the activation script defined — it restores your old PATH. The environment itself is untouched and you can re-activate later.
Try it yourself. Create a fresh folder, run python -m venv .venv, activate it, install requests, and run python -c "import requests; print(requests.__version__)". Then deactivate and confirm that python -c "import requests" either fails or reports a different version from your global install.
requirements.txt — the lowest common denominator
Once you have installed a few packages, freeze them:
pip freeze > requirements.txt
The output is a flat list of name==version pins for everything in the environment:
certifi==2024.6.2
charset-normalizer==3.3.2
idna==3.7
requests==2.32.3
urllib3==2.2.2
Anyone (including future you) can recreate the environment:
python -m venv .venv
source .venv/bin/activate
pip install -r requirements.txt
This file is not a manifest of what your project needs — it is a snapshot of what is installed, transitive dependencies included. For a true manifest, pyproject.toml is the modern answer — see Python Packages, pip, and pyproject.toml.
Pinning style
Three common patterns in requirements.txt:
requests==2.32.3 # exact pin — reproducible, brittle
requests~=2.32 # compatible release — allows 2.32.x but not 2.33
requests>=2.31,<3 # range — flexible, common for libraries
For applications, prefer exact pins generated by pip freeze (or a lockfile from a tool like uv or pip-tools). For libraries you publish, use ranges so consumers can resolve a compatible set.
Enter uv
uv is a newer tool, written in Rust, that replaces pip, venv, and virtualenv in one binary. It is dramatically faster — often 10x to 100x — and it resolves dependencies deterministically by default.
Install it once, globally:
# macOS / Linux
curl -LsSf https://astral.sh/uv/install.sh | sh
# or via pipx, brew, etc.
Then in any project:
# Create a venv (same .venv directory)
uv venv
# Install into it (no activation needed)
uv pip install requests
# Run a command inside the env
uv run python -c "import requests; print(requests.__version__)"
uv venv produces a .venv directory that is compatible with the standard one — you can still source .venv/bin/activate if you want. uv pip mimics pip closely enough that most workflows port over unchanged.
The bigger win is uv’s project mode, which uses pyproject.toml and a generated uv.lock file:
uv init my-app
cd my-app
uv add requests
uv run main.py
No activation, no manual requirements.txt, and the lockfile is checked into git. We cover the underlying pyproject.toml structure in the next post.
Try it yourself. Time it for yourself. In a folder, run time uv venv && time uv pip install fastapi. Then in another folder, run time python -m venv .venv && source .venv/bin/activate && time pip install fastapi. The gap is usually striking.
Where does pipx fit
venv is for project dependencies. pipx is for command-line tools — things you want available on your shell PATH everywhere, like black, ruff, httpie, or uv itself.
# Install a tool globally but isolated
pipx install ruff
pipx install httpie
# Upgrade
pipx upgrade ruff
# Remove
pipx uninstall ruff
Under the hood, pipx creates a dedicated venv per tool and symlinks the entry-point scripts into ~/.local/bin. You get global availability with none of the global-install pollution.
A useful split:
| Tool | Use for |
|---|---|
venv | Per-project libraries your code imports |
uv | Same, but faster — and with lockfiles |
pipx | CLI tools that should live outside any single project |
uv can also act like pipx via uv tool install ruff. If you adopt uv, you may not need pipx at all — but it is still the most widely understood tool for this job.
Common pitfalls
A few traps to sidestep:
- Committing
.venv/to git. Don’t. Add.venv/to.gitignore. The environment is reproducible fromrequirements.txtor the lockfile. - Mixing pythons. A venv is tied to the interpreter that created it. If you upgrade Python from 3.11 to 3.12, recreate the venv.
- Activating in a script.
source activateonly affects the current shell. In CI or Docker, prefer calling.venv/bin/pythondirectly — no activation needed. - Forgetting which env is active. If you see surprising
ImportErrors, runwhich pythonandpip listbefore debugging anything else.
A minimal project layout
A tidy starting point for any new Python project:
my-project/
├── .venv/ # local, gitignored
├── .gitignore # contains .venv/, __pycache__/, *.pyc
├── pyproject.toml # or requirements.txt for now
├── README.md
└── src/
└── my_project/
└── __init__.py
You can grow from requirements.txt to pyproject.toml later without rewriting code — only the install commands change.
Recap
You now know:
- A venv is a folder with its own interpreter and
site-packages python -m venv .venvcreates one;source .venv/bin/activateswitches into itpip freeze > requirements.txtsnapshots installed versions for reproducibilityuvis a faster, modern replacement that also handles lockfilespipx(oruv tool install) is for global CLI tools, not project dependencies- Never commit
.venv/; always recreate from the manifest
Next steps
Isolation is half the story. The other half is declaring what your project actually needs — name, version, dependencies, entry points — in a pyproject.toml. That is the standard manifest format for Python today, and the next post builds one from scratch.
→ Next: Python Packages, pip, and pyproject.toml
Questions or feedback? Email codeloomdevv@gmail.com.