02 · UV — The Python Package Manager
uv is a single, blazingly fast binary from Astral (the team behind Ruff) that replaces pip + virtualenv + pyenv + pipx + pip-tools + poetry. In April 2026 it's at version 0.11.7 and is the default recommendation for all new Python projects at IIT Madras.
Why UV?
If you've used Python for a while, you know the pain:
# The old way (circa 2023)
pyenv install 3.12
pyenv local 3.12
python -m venv .venv
source .venv/bin/activate
pip install -r requirements.txt # ⏳ 30 seconds
pip install poetry # 🤔 but then what about lockfiles?
Six tools. Slow. Error-prone. Every OS has quirks.
uv does all of this in one tool, 10–100× faster.
# The new way
uv init my-project
cd my-project
uv add fastapi uvicorn # ⚡ < 1 second
uv run python main.py
Install UV
UV is a standalone binary — it doesn't even need Python installed first (it'll install Python for you!).
macOS / Linux
curl -LsSf https://astral.sh/uv/install.sh | sh
Restart your shell or source ~/.bashrc / source ~/.zshrc.
Windows (PowerShell)
powershell -ExecutionPolicy ByPass -c "irm https://astral.sh/uv/install.ps1 | iex"
Via package managers
# macOS
brew install uv
# pipx (if you already have Python)
pipx install uv
# Cargo (if you have Rust)
cargo install --git https://github.com/astral-sh/uv uv
Verify:
uv --version
# uv 0.11.7
The Mental Model
UV has three ways to work. Pick the one that matches your task.
Way 1 — Single-File Scripts with Inline Dependencies
Perfect for quick data explorations or automation scripts.
# Create a script with dependencies declared inside
uv init --script example.py --python 3.12
uv add --script example.py requests rich
This adds a PEP 723 header to example.py:
# /// script
# requires-python = ">=3.12"
# dependencies = [
# "requests",
# "rich",
# ]
# ///
import requests
from rich import print
print(requests.get("https://api.github.com/zen").text)
Run it — UV handles the environment invisibly:
uv run example.py
# ✨ Creates a cached venv, installs requests + rich, runs the script
uv run auto-creates and caches a venv per set of dependencies. You never type source .venv/bin/activate again.
Way 2 — Full Projects with pyproject.toml
This is how real libraries and apps are structured.
uv init my-library --python 3.13
cd my-library
You get a scaffolded project:
my-library/
├── .git/
├── .gitignore
├── .python-version # says 3.13
├── main.py
├── pyproject.toml # project metadata
└── README.md
Add dependencies:
uv add fastapi "uvicorn[standard]" # runtime
uv add --dev pytest ruff mypy # dev-only
uv add --optional plot matplotlib # optional extra
This updates pyproject.toml:
[project]
name = "my-library"
version = "0.1.0"
requires-python = ">=3.13"
dependencies = [
"fastapi",
"uvicorn[standard]",
]
[dependency-groups]
dev = ["pytest", "ruff", "mypy"]
[project.optional-dependencies]
plot = ["matplotlib"]
And creates a lockfile (uv.lock) — the exact pinned versions of every transitive dependency. Commit this file.
Sync the environment to exactly match the lockfile:
uv sync # normal install
uv sync --frozen # use in CI — fails if lockfile is outdated
uv sync --all-groups # include dev deps
Run commands in the project's environment without activating:
uv run pytest
uv run uvicorn main:app --reload
uv run python -c "import fastapi; print(fastapi.__version__)"
Way 3 — Install CLI Tools Globally (pipx-style)
# Run once in a throwaway env (uvx = uv tool run)
uvx pycowsay "hello"
uvx ruff check .
# Install for reuse on your PATH
uv tool install ruff
uv tool install sqlite-utils
uv tool install datasette
# List installed tools
uv tool list
# Upgrade all tools
uv tool upgrade --all
Python Version Management — Forget About pyenv
# Install specific Python versions
uv python install 3.12 3.13 3.14
# List what's installed and available
uv python list
# Pin a project to a specific Python
uv python pin 3.13 # writes .python-version
# Run a one-off with a different interpreter
uv run --python 3.11 python --version
Essential Command Reference
| Command | What it does |
|---|---|
uv init <name> | Create a new project |
uv add <pkg> | Add a dependency + update lockfile + install |
uv add --dev <pkg> | Add a dev-only dependency |
uv remove <pkg> | Remove a dependency |
uv sync | Install exactly what's in uv.lock |
uv sync --frozen | Like uv sync but error if lockfile is stale (CI) |
uv lock | Refresh the lockfile without installing |
uv run <cmd> | Run a command inside the project env |
uv pip <cmd> | pip-compatible interface (for migration) |
uv python install <ver> | Install a Python version |
uv build | Build sdist + wheel into dist/ |
uv publish | Upload dist/* to PyPI |
uvx <tool> | Run a tool in an ephemeral env (= uv tool run) |
uv tool install <tool> | Install a CLI tool on your PATH |
uv cache clean | Clear the UV cache |
Migrating from requirements.txt
Already have a requirements.txt file? UV can consume it directly.
# Resolve a cross-platform universal requirements file
uv pip compile requirements.in --universal -o requirements.txt
# Create a venv and install
uv venv
uv pip sync requirements.txt
Or move to a pyproject.toml project:
uv init
# Paste each line of requirements.txt into `uv add`:
cat requirements.txt | xargs uv add
UV in CI (GitHub Actions)
name: CI
on: [push, pull_request]
jobs:
test:
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v4
- name: Install uv
uses: astral-sh/setup-uv@v7
with:
enable-cache: true # caches ~/.cache/uv
- name: Install Python
run: uv python install
- name: Install deps
run: uv sync --frozen
- name: Lint
run: uv run ruff check .
- name: Test
run: uv run pytest
No actions/setup-python needed — uv does that itself.
Workspaces (monorepos)
For a monorepo with multiple Python packages:
[tool.uv.workspace]
members = ["packages/*", "apps/*"]
my-monorepo/
├── pyproject.toml # workspace config
├── uv.lock # single shared lockfile
├── packages/
│ ├── core/
│ │ └── pyproject.toml
│ └── utils/
│ └── pyproject.toml
└── apps/
└── api/
└── pyproject.toml
Run commands per package:
uv run --package api uvicorn main:app
uv sync --all-packages
uv add --package api core # cross-package dep
Common Pitfalls
pip install into a uv-managed projectNever run pip install ... inside a project that already has a uv.lock. It will desynchronize the lockfile. Always use uv add, uv remove, or uv sync.
.venv/Your .gitignore should include .venv/. Commit pyproject.toml and uv.lock instead — they are the source of truth. Anyone who clones your repo runs uv sync and gets an identical env.
--frozenIn CI, use uv sync --frozen. This fails fast if someone adds a dep but forgets to commit uv.lock.
5-Minute Exercise
uv init hello-uv && cd hello-uvuv add requests- Write
main.py:pythonimport requestsprint(requests.get("https://api.github.com/zen").text) uv run main.py— should print a zen quote.- Inspect
uv.lock. Notice it pins not justrequestsbut also its transitive deps (urllib3,certifi, ...). - Commit
pyproject.tomlanduv.lockto Git. Don't commit.venv/.