Table of contents
What is uv?
Python's packaging story has historically been fragmented. You need pip to install packages,
venv or virtualenv to isolate environments, pyenv to manage Python versions,
and something like poetry or pipenv to get a proper lockfile. Each tool does its job, but
wiring them together adds friction — especially when onboarding new developers or setting up CI pipelines.
uv is a single tool that replaces all of them. It was built by Astral — the team behind
the ruff linter — and written entirely in Rust. It handles Python version management, virtual
environments, dependency resolution, package installation, and lockfiles under one unified interface.
The headline number: uv is typically 10 to 100 times faster than pip at installing packages. Cold installs that took 30 seconds with pip often finish in under a second with uv. That speed gain compounds in CI/CD environments where you're installing from scratch on every run.
Concepts
Before diving in, here are the key ideas that uv builds on:
Python version management — the ability to install and switch between different Python releases
(e.g., 3.11, 3.12, 3.13) on a single machine. uv handles this natively, replacing the need for pyenv.
Virtual environment — an isolated directory that contains its own Python interpreter and package set. Projects in different environments can use completely different dependency versions without interfering with each other.
Lockfile — a snapshot of the exact versions of every dependency (direct and transitive) that
were resolved at a point in time. uv.lock is uv's lockfile format. Committing it to version
control guarantees that every developer and every CI run installs the exact same packages.
pyproject.toml — the modern standard for declaring Python project metadata, build settings, and dependencies. uv uses it as the source of truth for your project's requirements.
Dependency resolution — the process of finding a compatible set of package versions that satisfies all declared constraints. uv's Rust-based resolver is significantly faster than pip's Python-based one, and it catches conflicts before they make it into your environment.
Installation and Setup
macOS and Linux
The recommended way to install uv is via the standalone installer script:
curl -LsSf https://astral.sh/uv/install.sh | sh
This installs the uv binary into ~/.local/bin and adds it to your PATH automatically.
Alternatively, if you're on macOS and prefer Homebrew:
brew install uv
Verify the installation:
uv --version
Windows
On Windows, use the PowerShell installer:
powershell -ExecutionPolicy ByPass -c "irm https://astral.sh/uv/install.ps1 | iex"
Or install via winget:
winget install --id=Astral.uv -e
Shell Completions
Enable tab completions for your shell. Run the appropriate command once and add the output to your shell config:
Bash:
echo 'eval "$(uv generate-shell-completion bash)"' >> ~/.bashrc
Zsh:
echo 'eval "$(uv generate-shell-completion zsh)"' >> ~/.zshrc
Fish:
echo 'uv generate-shell-completion fish | source' >> ~/.config/fish/config.fish
Restart your shell (or source the config) and you'll get completions for all uv subcommands and flags.
Managing Python Versions
One of uv's most useful features is built-in Python version management. No pyenv required.
Installing a Python Version
List the Python versions uv can install:
uv python list
Install a specific version:
uv python install 3.12
uv python install 3.11.9
Install multiple versions at once:
uv python install 3.11 3.12 3.13
uv downloads a standalone Python build (from the Astral-maintained python-build-standalone project)
and caches it. These installations are fully self-contained and don't touch your system Python.
Check which Python versions are currently managed by uv:
uv python list --only-installed
Pinning a Version per Project
Navigate to your project directory and pin a Python version:
uv python pin 3.12
This creates a .python-version file in the current directory:
3.12
Commit this file to your repository. Anyone who runs uv commands in this project will automatically use Python 3.12 — no manual configuration needed. uv will even download that version if it's not already installed locally.
Virtual Environments
Creating an Environment
Create a virtual environment in the current directory:
uv venv
This creates a .venv folder using the pinned Python version (from .python-version) or the
system default if no pin exists. To specify a version explicitly:
uv venv --python 3.12
To create it at a custom path:
uv venv my-env
Activate it the same way you would a standard venv:
# macOS / Linux
source .venv/bin/activate
# Windows
.venv\Scripts\activate
Auto-managed .venv
In project mode (after running uv init or working with a pyproject.toml), uv creates and
manages .venv automatically. You don't need to create or activate the environment manually —
commands like uv add, uv sync, and uv run all operate against the project's .venv directly.
This is a meaningful difference from traditional workflows where forgetting to activate the environment is a common source of confusion.
Differences from venv and virtualenv
uv venv produces a standard-compatible virtual environment — the structure is identical to what
python -m venv would create. The key differences are speed and integration.
uv creates the environment roughly 80× faster than python -m venv by avoiding the startup
overhead of Python itself (it's a compiled Rust binary). It also integrates with uv's package
installer and lockfile system, so the environment creation is just one step in a cohesive workflow
rather than a standalone operation.
Package Management
Adding and Removing Packages
Add a dependency to your project:
uv add requests
uv add "fastapi>=0.110"
uv add httpx --dev # development-only dependency
uv add does three things at once: it adds the package to pyproject.toml, resolves the full
dependency graph, updates uv.lock, and installs the package into .venv. One command, everything
stays in sync.
Remove a dependency:
uv remove requests
Same idea — it updates pyproject.toml, re-resolves the lockfile, and uninstalls the package.
Syncing the Environment
To make your local environment match exactly what's in uv.lock:
uv sync
This is the command you run after cloning a repo or pulling changes. It installs missing packages, removes packages that are no longer needed, and upgrades or downgrades anything that has drifted.
To include development dependencies:
uv sync --all-extras
To upgrade a specific package to the latest version that satisfies your constraints:
uv lock --upgrade-package requests
uv sync
Drop-in pip Replacement
If you have existing scripts or CI steps that use pip, uv provides a compatible interface:
uv pip install requests
uv pip install -r requirements.txt
uv pip uninstall requests
uv pip freeze
uv pip list
These commands behave identically to their pip counterparts but run at uv's speed.
This makes migration painless — swap pip for uv pip and everything just works faster.
Lockfiles
uv.lock is generated automatically whenever you add, remove, or upgrade a dependency.
It records the exact version of every package in the dependency tree — direct dependencies
and all their transitive dependencies.
A snippet of what uv.lock looks like:
version = 1
requires-python = ">=3.12"
[[package]]
name = "requests"
version = "2.31.0"
source = { registry = "https://pypi.org/simple" }
dependencies = [
{ name = "certifi" },
{ name = "charset-normalizer" },
{ name = "idna" },
{ name = "urllib3" },
]
Always commit uv.lock to version control. It's the source of truth for reproducible installs —
running uv sync on any machine will produce an environment with precisely these versions.
Project Workflow
Scaffolding with uv init
Start a new project from scratch:
uv init my-project
cd my-project
uv creates this structure:
📁 my-project/
├── 📄 .python-version ← pins the Python version
├── 📄 pyproject.toml ← project metadata and dependencies
├── 📄 README.md
└── 📄 hello.py ← starter script
The environment and lockfile are created on demand the first time you run uv add or uv sync.
You can also initialize uv in an existing project directory:
cd existing-project
uv init
This adds pyproject.toml and .python-version without touching your existing files.
pyproject.toml Integration
uv uses pyproject.toml as the single source of truth for your project. After running
uv init and adding a few packages, the file looks like this:
[project]
name = "my-project"
version = "0.1.0"
description = ""
requires-python = ">=3.12"
dependencies = [
"fastapi>=0.110.0",
"httpx>=0.27.0",
]
[tool.uv]
dev-dependencies = [
"pytest>=8.0",
"ruff>=0.4",
]
Direct dependencies are listed under [project].dependencies. Development-only packages
go under [tool.uv].dev-dependencies. The lockfile captures everything else.
Running Scripts
Use uv run to execute a script or command inside the project environment without activating it:
uv run python hello.py
uv run pytest
uv run ruff check .
uv ensures the environment is up to date before running the command, so you never have to
remember to call uv sync first. It's a safe and convenient entry point for all project commands.
You can also run one-off scripts with inline dependency declarations — no project needed:
uv run --with httpx python -c "import httpx; print(httpx.get('https://httpbin.org/get').status_code)"
uv installs httpx into a temporary, isolated environment for that single invocation.
uv vs The Alternatives
| Tool | Python versions | Virtualenvs | Packages | Lockfile | Speed |
|---|---|---|---|---|---|
| pip + venv | No | Yes | Yes | No (manual freeze) | Baseline |
| poetry | No (needs pyenv) | Yes | Yes | Yes | Slow-medium |
| pipenv | No (needs pyenv) | Yes | Yes | Yes (Pipfile.lock) | Slow |
| uv | Yes | Yes | Yes | Yes (uv.lock) | 10–100× faster |
uv vs pip + venv — pip and venv together cover the basics but require you to manage
the workflow yourself: create the environment, activate it, install packages, manually freeze
a requirements.txt. There's no lockfile, no built-in Python version management, and no
single command to restore a reproducible environment. uv handles all of this natively.
uv vs poetry — poetry is the closest spiritual predecessor to uv in terms of scope.
It manages dependencies, lockfiles, and environments, and has a polished CLI. The main drawbacks
are speed and the need to pair it with pyenv for Python version management. uv is substantially
faster at every operation and handles Python versions directly.
uv vs pipenv — pipenv introduced the concept of a Pipfile + Pipfile.lock to Python
and was once the recommended tool. Over time it fell out of favor due to slow resolution, bugs,
and inconsistent behavior. uv covers everything pipenv did, faster and more reliably.
When to stay with pip — if you're working in an environment where you cannot install additional
tooling, or maintaining a legacy project where changing the workflow would require significant
coordination, plain pip with a requirements.txt is still perfectly valid. uv's uv pip
interface lets you migrate incrementally in those cases.
CI/CD Integration
GitHub Actions
Astral maintains an official GitHub Action for installing uv. Here's a complete workflow that installs uv, syncs the project, and runs tests:
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@v4
with:
enable-cache: true
- name: Set up Python
run: uv python install
- name: Install dependencies
run: uv sync --all-extras
- name: Run tests
run: uv run pytest
The enable-cache: true option caches the uv download cache between runs. Combined with
the lockfile, this means subsequent CI runs skip most network requests entirely — only changed
packages are re-downloaded.
Docker
A minimal Dockerfile that uses uv for a production Python application:
FROM python:3.12-slim
# Install uv
COPY --from=ghcr.io/astral-sh/uv:latest /uv /uvx /bin/
WORKDIR /app
# Copy dependency files first for better layer caching
COPY pyproject.toml uv.lock ./
# Install dependencies into the system Python (no virtualenv needed in Docker)
RUN uv sync --frozen --no-dev --system
# Copy application code
COPY . .
CMD ["python", "main.py"]
Key flags explained:
--frozen— refuses to updateuv.lock; fails if the lockfile is out of sync. This guarantees the Docker image always installs exactly what was locked.--no-dev— skips development-only dependencies.--system— installs into the system Python instead of creating a.venv. Appropriate for containers where isolation is already provided by Docker.
Tips and Gotchas
Migrating from pip + requirements.txt — if you have an existing requirements.txt, you can
import it into a uv project:
uv add -r requirements.txt
This adds all listed packages to pyproject.toml and generates a proper uv.lock. After
verifying everything works, you can retire the requirements.txt.
Migrating from poetry — pyproject.toml structure from poetry is largely compatible. uv reads
the [project].dependencies table directly. The main difference is the [tool.poetry.*] sections,
which you can convert to their [project.*] equivalents. Run uv sync after to generate uv.lock.
uv.lock merge conflicts — because uv.lock is a structured text file, merge conflicts
can occur when two branches modify dependencies. The cleanest resolution is to accept one side of the
conflict and re-run uv lock to regenerate the file from pyproject.toml. Don't try to
manually resolve the lockfile.
Useful flags you might miss:
uv add requests --optional http # add to an optional dependency group
uv sync --no-install-project # install dependencies but not the project package itself
uv tree # print the full dependency tree
uv lock --check # verify lockfile is up to date without modifying it
uv cache clean # clear the uv package cache
uv doesn't need a project — you can use uv pip install and uv venv in any directory
without a pyproject.toml. The project-level features (uv add, uv sync, uv run) require
one, but the low-level pip-compatible interface works anywhere.
Global tools — uv can install CLI tools globally so they're available system-wide without polluting any project environment:
uv tool install ruff
uv tool install black
uv tool list
Real-World Example
Let's build a small FastAPI application from scratch using only uv.
Step 1: Create the project
uv init fastapi-demo
cd fastapi-demo
uv python pin 3.12
Step 2: Add dependencies
uv add fastapi uvicorn[standard]
uv add --dev pytest httpx
After these commands, pyproject.toml contains the declared dependencies and uv.lock
has the full resolved graph.
Step 3: Write the application
Create main.py:
from fastapi import FastAPI
app = FastAPI()
@app.get("/")
def read_root() -> dict[str, str]:
return {"message": "Hello from uv + FastAPI"}
@app.get("/items/{item_id}")
def read_item(item_id: int, q: str | None = None) -> dict:
return {"item_id": item_id, "q": q}
Step 4: Write a test
Create test_main.py:
from fastapi.testclient import TestClient
from main import app
client = TestClient(app)
def test_read_root():
response = client.get("/")
assert response.status_code == 200
assert response.json() == {"message": "Hello from uv + FastAPI"}
def test_read_item():
response = client.get("/items/42?q=hello")
assert response.status_code == 200
data = response.json()
assert data["item_id"] == 42
assert data["q"] == "hello"
Step 5: Run the tests
uv run pytest
Step 6: Start the server
uv run uvicorn main:app --reload
No activation needed. The final project structure looks like this:
📁 fastapi-demo/
├── 📄 .python-version ← 3.12
├── 📄 pyproject.toml ← declared dependencies
├── 📄 uv.lock ← locked dependency graph
├── 📄 main.py
├── 📄 test_main.py
└── 📁 .venv/ ← auto-managed, not committed
Anyone who clones this repository can get a fully working environment with a single command:
uv sync
Summary
uv collapses what used to be a four-tool chain — pyenv, venv, pip, and poetry or pipenv — into
a single, fast binary. It manages Python versions, creates and maintains virtual environments,
resolves and locks dependencies, and provides a drop-in pip-compatible interface for gradual
adoption.
The speed improvement is real and compounds over time. Install times that cost minutes in CI
pipelines drop to seconds. Local uv sync after a git pull is nearly instant.
For new projects, starting with uv init gives you a clean, modern Python workflow from the
first command. For existing projects, the uv pip interface makes migration a one-line change.
Either way, the lockfile ensures that what works on your machine works everywhere else too.
