uv: Python's Package Manager Finally Doesn't Suck

pip is slow. virtualenv is clunky. pyenv is fragile. pipx is fine until it isn't. uv replaces all four with a single Rust binary that installs packages in milliseconds instead of seconds.

That's not marketing copy. I timed it.

What uv Replaces

Before uv, a Python project required a stack of tools that barely talked to each other. pyenv to manage Python versions. virtualenv or venv to create isolated environments. pip to install packages. pip-tools or poetry or pipenv to lock dependencies. pipx for CLI tools. Each one maintained separately, each with its own configuration format, each capable of breaking the others in subtle ways.

uv collapses that entire stack into one tool:

  • Package installation: uv pip install (drop-in pip replacement)
  • Virtual environments: uv venv (creates envs in ~50ms)
  • Python version management: uv python install 3.12 (replaces pyenv)
  • Dependency locking: uv lock (generates a cross-platform lockfile)
  • Script running: uv run (inline dependency resolution)
  • Tool management: uv tool install (replaces pipx)

One binary. One config format. One dependency resolver. The cognitive overhead drops from five mental models to one.

The Speed Difference

The performance gap between uv and pip is not incremental. It's a category difference.

I installed the same set of packages — requests, flask, sqlalchemy, pandas, and their dependencies — using both tools on a clean environment. Same machine, same Python version, same network.

# pip (cold cache)
$ time pip install requests flask sqlalchemy pandas
real    12.4s

# uv (cold cache)
$ time uv pip install requests flask sqlalchemy pandas
real    0.81s

# pip (warm cache)
$ time pip install requests flask sqlalchemy pandas
real    4.2s

# uv (warm cache)
$ time uv pip install requests flask sqlalchemy pandas
real    0.11s

Cold cache: 15x faster. Warm cache: 38x faster. These numbers are consistent across different package sets. Small installs see even larger multipliers because pip's startup overhead is a fixed cost that dominates when the actual work is minimal.

Creating a virtual environment tells the same story:

# python -m venv
$ time python -m venv .venv
real    3.1s

# uv venv
$ time uv venv
real    0.02s

Twenty milliseconds versus three seconds. That might not matter once, but it changes behavior. When creating a venv takes three seconds, you think twice about throwaway environments. When it takes twenty milliseconds, you create one for every experiment. The speed difference isn't a performance optimization. It changes how you work.

Installation

curl -LsSf https://astral.sh/uv/install.sh | sh

Or with Homebrew:

brew install uv

No Python required. uv is a standalone Rust binary — it doesn't depend on the thing it manages. That means you can use it to bootstrap Python itself on a fresh machine:

uv python install 3.12

Compare that to pyenv, which compiles Python from source (requiring a C compiler, OpenSSL headers, and five minutes of your life) or manages shims that break in unexpected ways. uv downloads prebuilt Python binaries from the python-build-standalone project. The install takes seconds.

The Killer Feature

uv run with inline script dependencies changes how you write one-off Python tools.

Standard approach: create a directory, create a venv, activate it, install packages, write your script, run it, forget to clean up, discover 47 abandoned venvs six months later. With uv, the dependencies live inside the script itself.

# /// script
# requires-python = ">=3.11"
# dependencies = [
#   "httpx",
#   "rich",
# ]
# ///

import httpx
from rich.console import Console
from rich.table import Table

console = Console()
resp = httpx.get("https://api.github.com/repos/astral-sh/uv/releases/latest")
data = resp.json()

table = Table(title="Latest uv Release")
table.add_column("Field")
table.add_column("Value")
table.add_row("Version", data["tag_name"])
table.add_row("Published", data["published_at"][:10])
table.add_row("Assets", str(len(data["assets"])))

console.print(table)

Save that as check_uv.py and run it:

uv run check_uv.py

uv reads the inline metadata, creates a temporary environment, installs httpx and rich, runs the script, and cleans up. No venv to manage. No requirements.txt to maintain. The script is self-contained — you can send it to someone and they run it with a single command, assuming they have uv installed.

I use this pattern constantly for data processing scripts, API clients, and automation tools that don't deserve their own project. A script that fetches data from an API, transforms it with pandas, and writes a CSV. A quick Playwright script to check if a site is responding. A one-off migration that needs SQLAlchemy. Each one carries its own dependencies and runs anywhere uv is installed.

Project Management

For proper projects (not one-off scripts), uv handles the full lifecycle:

# Start a new project
uv init my-project
cd my-project

# Add dependencies
uv add flask sqlalchemy

# Add dev dependencies
uv add --dev pytest ruff

# Lock dependencies (cross-platform)
uv lock

# Run your app
uv run flask run

# Run tests
uv run pytest

The pyproject.toml is the single source of truth. The uv.lock file pins exact versions across platforms. uv sync installs exactly what's in the lockfile. No more "works on my machine" because someone had a different version of a transitive dependency.

This is roughly what Poetry was trying to be, except uv resolves dependencies in under a second where Poetry regularly takes 30-60 seconds on complex dependency trees. The resolver is written in Rust using the PubGrub algorithm — the same approach used by Dart's package manager — and it handles conflict resolution that makes pip's backtracking resolver give up entirely.

The Gotcha

uv is opinionated about virtual environments in ways that can surprise you if you're coming from pip.

Running uv pip install outside a virtual environment fails by default. pip would install to your system Python (or user site-packages with --user). uv refuses. It wants you in a venv, and it will tell you so.

$ uv pip install requests
error: No virtual environment found

This is the right behavior — installing packages into your system Python is the source of most Python dependency nightmares — but it catches people off guard. The fix is either create a venv first (uv venv && source .venv/bin/activate) or use uv run which handles environments automatically.

The other friction point: not every pip flag has a uv equivalent. uv pip covers the common cases but intentionally doesn't replicate pip's full surface area. Editable installs, constraint files, and some index configuration options work differently or have different syntax. If you have a complex pip.conf or heavily customized pip workflow, expect to spend an hour translating it.

And if you're using conda for scientific computing with compiled dependencies (numpy built against MKL, CUDA-specific PyTorch builds), uv isn't a replacement. It manages Python packages from PyPI. It doesn't manage system libraries or compiled binary blobs that conda handles. For data science work that needs specific BLAS implementations or GPU drivers, conda still has a role.

uv vs the Alternatives

Feature pip + venv Poetry uv
Install speed Slow (seconds) Slow (seconds) Fast (milliseconds)
Dependency resolution Backtracking (fragile) SAT solver (slow) PubGrub (fast, correct)
Lockfile pip freeze (not cross-platform) poetry.lock uv.lock (cross-platform)
Python management No (need pyenv) No (need pyenv) Built-in
Script runner No No uv run (inline deps)
Tool install No (need pipx) No (need pipx) uv tool install
Written in Python Python Rust
Config format requirements.txt pyproject.toml pyproject.toml

pip still works fine for simple cases. Poetry still works fine if your team is already using it. But for new projects and solo builders setting up their Python stack from scratch, uv is the obvious choice. It's faster at everything, it replaces more tools, and it's backed by Astral (the same team behind Ruff, the Python linter that replaced flake8 and isort and black for most projects).

What This Means for Solo Builders

Python's packaging story has been a running joke in the programming community for fifteen years. Every year brought a new tool that was supposed to fix it — setuptools, distribute, pip, pipenv, poetry, pdm, hatch — and every tool added complexity without fully replacing what came before.

uv is the first tool that actually simplifies the stack instead of adding another layer to it. One binary replaces five tools. Millisecond operations replace multi-second waits. Self-contained scripts replace the venv-activate-install-run dance.

For solo builders running Python in production — automation scripts, data pipelines, API backends, CLI tools — the reduction in friction compounds. Every script you write is portable. Every environment is reproducible. Every install is fast enough that you stop noticing it.

The Python packaging problem isn't fully solved. Conda still owns the scientific computing niche. Legacy projects with complex setuptools configurations will take time to migrate. But for new work, the answer is uv.

One tool. One config file. Milliseconds instead of seconds.