uv: The Python Package Manager That Makes pip Feel Ancient
uv: The Python Package Manager That Makes pip Feel Ancient
uv is a Python package manager and project tool written in Rust by Astral, the same team behind Ruff. It replaces pip, pip-tools, virtualenv, pyenv, and most of Poetry's functionality with a single binary that runs 10-100x faster. If you have used Cargo for Rust or Bun for JavaScript, uv brings that same "it just works and it's fast" experience to Python.
This guide covers practical daily usage, project setup, migration from existing tools, and the features that make uv worth switching to today.
Why uv Exists
Python packaging has been a mess for over a decade. The standard workflow involved juggling multiple tools:
- pyenv to install and manage Python versions
- virtualenv or venv to create isolated environments
- pip to install packages
- pip-tools (pip-compile) to lock dependencies
- Poetry or PDM to handle all of the above in one tool, slowly
Each tool had its own quirks, its own configuration format, and its own failure modes. Dependency resolution in pip was notoriously broken until relatively recently, and even now it is slow.
uv replaces the entire stack. It installs Python versions, creates virtual environments, resolves dependencies, manages lockfiles, and runs scripts -- all from one command-line tool that finishes before pip would have even started resolving.
Installation
# Standalone installer (recommended)
curl -LsSf https://astral.sh/uv/install.sh | sh
# Via Homebrew
brew install uv
# Via pip (if you must)
pip install uv
# Via mise
mise use uv@latest
After installation, uv is a single static binary. No runtime dependencies, no bootstrapping issues.
Project Setup
Starting a New Project
# Create a new Python project
uv init my-project
cd my-project
This generates a pyproject.toml, a hello.py entry point, and a .python-version file. The pyproject.toml uses standard PEP 621 metadata -- no proprietary format.
[project]
name = "my-project"
version = "0.1.0"
description = "Add your description here"
readme = "README.md"
requires-python = ">=3.12"
dependencies = []
Managing Dependencies
# Add a dependency
uv add requests
uv add fastapi uvicorn
# Add a dev dependency
uv add --dev pytest ruff mypy
# Add with version constraints
uv add "sqlalchemy>=2.0,<3.0"
# Remove a dependency
uv remove requests
Every uv add and uv remove updates pyproject.toml, regenerates the lockfile (uv.lock), and syncs the virtual environment. One command does what used to take three steps.
The Lockfile
uv generates a uv.lock file that pins exact versions of every dependency, including transitive ones. This lockfile is cross-platform -- it records the correct versions for Linux, macOS, and Windows so that uv sync works regardless of where you run it.
# Regenerate lockfile from pyproject.toml
uv lock
# Sync environment to match lockfile exactly
uv sync
# Sync including dev dependencies (default)
uv sync --all-extras
Commit uv.lock to version control. This is your reproducible build guarantee.
Running Things
Scripts and Commands
# Run a script in the project environment
uv run python main.py
# Run a module
uv run python -m pytest
# Run an installed CLI tool
uv run ruff check .
# Run with additional dependencies (without installing globally)
uv run --with rich python -c "from rich import print; print('[bold]Hello[/bold]')"
uv run automatically creates and activates the virtual environment if it does not exist. You never need to manually run source .venv/bin/activate again.
Inline Script Dependencies
uv supports PEP 723 inline metadata, which means you can declare dependencies directly in a Python script:
# /// script
# requires-python = ">=3.12"
# dependencies = [
# "httpx",
# "rich",
# ]
# ///
import httpx
from rich import print
resp = httpx.get("https://api.github.com/repos/astral-sh/uv")
print(f"[bold]Stars:[/bold] {resp.json()['stargazers_count']}")
Run it with uv run script.py and uv handles dependency installation automatically. This is incredibly useful for one-off scripts and utilities.
Python Version Management
uv replaces pyenv for managing Python installations:
# Install a Python version
uv python install 3.12
uv python install 3.13
# List installed versions
uv python list
# Pin project to a specific version
uv python pin 3.12
# Use a specific version for a command
uv run --python 3.13 python -c "import sys; print(sys.version)"
Python versions are downloaded from Astral's standalone builds, which are pre-compiled and install in seconds rather than the minutes it takes pyenv to compile from source.
Tool Management
uv also replaces pipx for running and installing Python CLI tools:
# Run a tool without installing it
uvx ruff check .
uvx black --check .
# Install a tool globally
uv tool install ruff
uv tool install httpie
# List installed tools
uv tool list
# Upgrade tools
uv tool upgrade ruff
uvx is an alias for uv tool run. It downloads the tool into an isolated environment and runs it immediately. No global pollution, no conflicts.
Migrating from Existing Tools
From pip + requirements.txt
# Initialize a uv project in an existing directory
uv init
# Import existing requirements
uv add -r requirements.txt
uv add --dev -r requirements-dev.txt
# The old files are now redundant -- uv.lock replaces them
From Poetry
# uv reads pyproject.toml directly
# If your project uses Poetry's [tool.poetry] section,
# convert to standard PEP 621 [project] metadata:
# Poetry format:
# [tool.poetry.dependencies]
# python = "^3.12"
# requests = "^2.31"
# Standard format (what uv uses):
# [project]
# requires-python = ">=3.12"
# dependencies = ["requests>=2.31"]
After converting the metadata format, run uv lock to generate the lockfile and uv sync to create the environment.
From pip-tools
If you were using pip-compile to generate locked requirements files, uv lock is the direct replacement. The lockfile format is different but serves the same purpose -- pinning exact versions for reproducible installs.
CI Integration
# GitHub Actions
- name: Install uv
uses: astral-sh/setup-uv@v5
- name: Install dependencies
run: uv sync
- name: Run tests
run: uv run pytest
- name: Check formatting
run: uv run ruff format --check .
uv's speed makes CI significantly faster. A cold install that takes pip 45 seconds typically finishes in under 5 seconds with uv, even with a clean cache.
Configuration
uv reads configuration from pyproject.toml or uv.toml:
[tool.uv]
# Use a specific index
index-url = "https://pypi.org/simple"
# Extra indexes (e.g., private packages)
extra-index-url = ["https://pypi.company.com/simple"]
# Exclude packages from resolution
override-dependencies = ["numpy<2.0"]
Workspace Support
For monorepos, uv supports workspaces:
# Root pyproject.toml
[tool.uv.workspace]
members = ["packages/*", "services/*"]
Each member has its own pyproject.toml but shares a single lockfile and virtual environment. This is similar to Cargo workspaces or npm workspaces.
Performance Comparison
On a real project with ~150 dependencies:
| Operation | pip | Poetry | uv |
|---|---|---|---|
| Cold install | 48s | 62s | 3.2s |
| Cached install | 12s | 18s | 0.8s |
| Lock/resolve | 35s | 45s | 1.1s |
| Add one package | 15s | 25s | 0.9s |
These are not synthetic benchmarks. uv is genuinely 10-50x faster for every operation. The difference is large enough to change how you work -- you stop batching dependency changes and just add packages as you need them.
The Bottom Line
uv is not an incremental improvement over pip. It is a complete replacement for the fragmented Python packaging toolchain, and it is faster at every task by an order of magnitude. If you are starting a new Python project, use uv from the beginning. If you are maintaining an existing project, the migration takes minutes and the speed improvements are immediate. Astral has effectively solved Python packaging, and the answer was "rewrite everything in Rust."