Skip to content
C Codeloom
Python

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.

·7 min read · By Yash Kesharwani
Intermediate 9 min read

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

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-packages directory
  • A small pyvenv.cfg file

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:

ToolUse for
venvPer-project libraries your code imports
uvSame, but faster — and with lockfiles
pipxCLI 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 from requirements.txt or 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 activate only affects the current shell. In CI or Docker, prefer calling .venv/bin/python directly — no activation needed.
  • Forgetting which env is active. If you see surprising ImportErrors, run which python and pip list before 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 .venv creates one; source .venv/bin/activate switches into it
  • pip freeze > requirements.txt snapshots installed versions for reproducibility
  • uv is a faster, modern replacement that also handles lockfiles
  • pipx (or uv 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.