← All articles
LANGUAGES uv: The Python Package Manager That Makes pip Feel A... 2026-02-14 · 5 min read · uv · python · package-manager

uv: The Python Package Manager That Makes pip Feel Ancient

Languages 2026-02-14 · 5 min read uv python package-manager astral pip virtual-environments dependency-management

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.

uv logo by Astral

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:

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