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.py

uv — The Future of Python Tooling (Astral)

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.

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 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

?> 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-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#

CommandWhat 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 syncInstall exactly what’s in uv.lock
uv sync --frozenLike uv sync but error if lockfile is stale (CI)
uv lockRefresh 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 buildBuild sdist + wheel into dist/
uv publishUpload 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 cleanClear 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#

!> 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#

  1. uv init hello-uv && cd hello-uv
  2. uv add requests
  3. Write main.py:
    import requests
    print(requests.get("https://api.github.com/zen").text)
  4. uv run main.py — should print a zen quote.
  5. Inspect uv.lock. Notice it pins not just requests but also its transitive deps (urllib3, certifi, …).
  6. Commit pyproject.toml and uv.lock to Git. Don’t commit .venv/.

Further Reading#