02 · UV — The Python Package Manager#
?> TL;DR
?> 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.pyInstall 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 | shRestart 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 uvVerify:
uv --version
# uv 0.11.7The Mental Model#
UV has three ways to work. Pick the one that matches your task.
graph TD
A[Starting a task?] --> B{Single .py file<br/>or full project?}
B -->|Single file| C[uv run script.py<br/>Inline deps]
B -->|Project| D[uv init + uv add<br/>pyproject.toml]
A --> E[Want to use a CLI tool<br/>like ruff or black?]
E --> F[uvx or<br/>uv tool install]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 richThis 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?> No more activating virtualenvs
?> 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-libraryYou get a scaffolded project:
my-library/
├── .git/
├── .gitignore
├── .python-version # says 3.13
├── main.py
├── pyproject.toml # project metadata
└── README.mdAdd dependencies:
uv add fastapi "uvicorn[standard]" # runtime
uv add --dev pytest ruff mypy # dev-only
uv add --optional plot matplotlib # optional extraThis 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 depsRun 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 --allPython 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 --versionEssential 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.txtOr move to a pyproject.toml project:
uv init
# Paste each line of requirements.txt into `uv add`:
cat requirements.txt | xargs uv addUV 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 pytestNo 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.tomlRun commands per package:
uv run --package api uvicorn main:app
uv sync --all-packages
uv add --package api core # cross-package depCommon Pitfalls#
!> Don’t mix pip install into a uv-managed project
!> Never 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.
!> Don’t commit .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.
?> CI always uses --frozen
?> In 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:import requests print(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/.
Further Reading#
- UV official docs
- UV GitHub repo
- PEP 723 — Inline Script Metadata
- Trusted publishing guide — used in Lab 1.1
