uv — the Rust-based Python package manager basics

uv — Rust 製 Python パッケージマネージャの基礎

article technology high #uv#python#package-manager#rust#astral#dependency-management#virtualenv#pep-723
Created: 2026-04-19 Updated:

uv is Astral's Rust-built Python package and project manager that collapses pip, pip-tools, pipx, virtualenv, pyenv, and twine into one static binary. A content-addressed cache, PubGrub resolver, and `uv run`-centric project workflow yield 10-100x faster installs and deterministic, cross-platform `uv.lock` reproducibility.

uv — the Rust-based Python package manager basics

uv is a single Rust binary from Astral that absorbs the entire Python tooling chain — pip, pip-tools, pipx, virtualenv, pyenv, twine, and the project-manager role of Poetry or PDM — behind two surfaces stacked on one PubGrub resolver: a pip-compatible shim (uv pip) for legacy workflows and a project interface (uv init, add, sync, run, lock) that owns pyproject.toml, uv.lock, and .venv/. A global content-addressed cache with hardlinks, native concurrency, and zero interpreter startup yield the often-cited 10-100x speed-up over pip while keeping the lockfile deterministic across platforms.

What uv is and what it replaces

uv ships as a single static binary distributed for macOS, Linux, and Windows with no Python or Rust prerequisite. Astral — the Ruff team — designed it to terminate the Python tooling diaspora: a fresh install of uv plus the standard library is enough to bootstrap a project, manage interpreters, install CLI tools, build wheels, and publish to PyPI. The official scope explicitly covers pip, pip-tools, pipx, poetry, pyenv, twine, and virtualenv, replacing each with a uv subcommand: uv pip, uv tool/uvx, uv python, uv build, uv publish, and the project commands. The mental model that pays off for a senior engineer is two surfaces on one resolver. The first surface is uv pip ..., a near-drop-in CLI for one-off installs into an arbitrary environment — the migration path from existing requirements.txt-based stacks. The second is the project interface (uv init, uv add, uv sync, uv run, uv lock) that owns pyproject.toml, uv.lock, and .venv/ end-to-end, occupying the same niche as Poetry or PDM. Both surfaces share the resolver, the installer, and the global cache, so switching between them inside a session costs nothing. The trade-off uv makes is the same one Cargo made for Rust: a single vendor-controlled binary in place of a composable Unix-style chain, exchanging breadth of plug-in points for coherence and speed.

Why it’s fast (the design choices, not just the number)

The headline claim is 10-100x faster than pip on installs and resolution, with cold-cache resolution typically completing in a fraction of a second. That number is a consequence of stacking several design decisions, not a single optimisation. The core is written in Rust with native concurrency: the resolver and installer run without the GIL, and network fetches, metadata parsing, and wheel unpacking overlap freely. The resolver itself is PubGrub via pubgrub-rs — the same incremental algorithm Cargo and Dart’s pub use — which produces clear conflict explanations and avoids the exponential blow-ups of naive backtracking. A global content-addressed cache (default ~/.cache/uv) deduplicates wheels across every project on the machine and links files into each .venv/ with hardlinks or reflinks rather than copying, so each additional project costs near-zero disk and a uv sync reuses bytes already on disk. Aggressive HTTP and metadata caching means registry responses honour cache headers, Git dependencies key on resolved commit hashes, and local sources key on the mtime of the archive or pyproject.toml — repeat resolutions are essentially free. Finally, because uv is itself a native binary, there is no per-invocation Python import cost; uv run in a tight CI loop pays no startup tax. The net effect is that a fresh uv sync often completes faster than pip can finish importing its own modules.

The project workflow (uv init, add, sync, run, lock)

The project surface is the recommended path for new code and the only path that maintains a reproducible lock.

uv init my-app                 # scaffold pyproject.toml, main.py, .python-version
cd my-app
uv add 'fastapi' 'pydantic>2'
uv add --dev pytest ruff
uv add 'requests==2.31.0'
uv add 'git+https://github.com/psf/requests'
uv add -r requirements.txt     # bulk-import existing deps
uv remove pydantic
uv lock                        # regenerate uv.lock from pyproject.toml
uv lock --upgrade-package requests
uv sync                        # materialize the locked env into .venv/
uv sync --no-dev
uv sync --group lint --group test
uv sync --locked               # CI gate: fail if lock is stale
uv run pytest                  # execute inside the project env
uv run python -m my_app

Two behavioural details matter. First, uv run is the default execution path: before each invocation, uv re-checks that uv.lock is in sync with pyproject.toml and that .venv/ matches the lock, reconciling automatically if anything drifted. Activating the venv with source .venv/bin/activate is no longer needed and is actively discouraged in the official migration guide. Second, uv add writes through — it updates pyproject.toml, regenerates uv.lock, and installs into .venv/ in a single command, eliminating the separate lock then install dance familiar from Poetry’s older flows. A standard project after uv init plus a few uv add calls contains pyproject.toml, uv.lock (commit it), .python-version, and a .venv/ (do not commit). uv adopts the standardised PEP 735 [dependency-groups] table for non-runtime deps such as dev, lint, and docs rather than a vendor-specific namespace, and a [tool.uv.sources] table overrides where named dependencies come from (Git, path, workspace member, alternative index) without polluting public [project.dependencies]. uv build --no-sources strips those overrides for publishable artifacts.

The pip compatibility shim (uv pip)

uv pip is a near-drop-in replacement for the pip CLI that uses uv’s resolver and installer underneath. It is the right tool when an existing requirements.txt workflow has not yet migrated to projects, or when a one-off install needs to land in a system Python or arbitrary venv.

uv pip install flask 'flask[async]' 'ruff>=0.2.0'
uv pip install "git+https://github.com/astral-sh/ruff@v0.2.0"
uv pip install -r requirements.txt
uv pip install -r pyproject.toml --extra dev --all-extras
uv pip install -e .
uv pip install --system flask        # install into system Python (CI / Docker)
uv pip uninstall flask ruff
uv pip list / show / check / freeze
uv pip compile -o requirements.txt pyproject.toml

The crucial property: uv pip does not read or write uv.lock and does not auto-create .venv/. It is a faster pip, not a project manager. For reproducible environments, the project commands (uv add, uv sync) are the supported path. Mixing uv pip install calls with project commands inside the same checkout is a reliable way to make .venv/ drift from uv.lock; uv will silently install whatever uv pip is told to install, and the next uv run will reconcile by reinstalling from the lock and quietly removing whatever was added out-of-band. Treat uv pip and the project commands as belonging to disjoint repositories. uv pip compile plays a second role as the interop bridge: it produces a flat requirements.txt that any pip-based downstream toolchain can consume, which matters because uv.lock is uv-specific and not portable to other resolvers. For environment selection, the rules are conventional but easy to forget: by default uv pip targets the active virtual environment, falls back to .venv/ in the current directory, and otherwise raises; --system, --python <interpreter>, and VIRTUAL_ENV adjust this in the obvious ways.

Tools and Python version management (uv tool, uvx, uv python)

uv replaces pipx with two related commands. uv tool install places a CLI tool in an isolated environment under ~/.local/share/uv/tools (or the platform equivalent) and links its entry points onto PATH, exactly like pipx but with uv’s installer underneath. uvx is an alias for uv tool run, which executes a tool in an ephemeral environment without persisting it — ideal for ad-hoc invocations.

uv tool install ruff
uv tool install 'httpie>0.1.0'
uv tool install mkdocs --with mkdocs-material   # plugins
uv tool install git+https://github.com/httpie/cli
uv tool list / upgrade ruff / uninstall ruff

uvx ruff check .
uvx ruff@0.3.0 check .
uvx --from httpie http example.com              # entry point name differs from package
uvx --from 'mypy[faster-cache,reports]' mypy --xml-report report
uvx --with mkdocs-material mkdocs serve

Ephemeral environments are themselves cached, so repeated uvx calls of the same tool are nearly instant after the first run.

uv also subsumes pyenv via uv python. uv ships its own Python distributions — the python-build-standalone builds — and downloads, pins, and upgrades them on demand. There is no shim layer: when a project’s requires-python or .python-version references an interpreter that is not installed, the next uv run or uv sync downloads it transparently.

uv python install                   # latest CPython
uv python install 3.11 3.12 3.13
uv python install pypy@3.10
uv python list / list --managed-python
uv python pin 3.12                  # writes .python-version locally
uv python pin --global 3.12
uv python upgrade 3.12              # patch-level upgrade
uv python uninstall 3.11

The combination of bundled interpreters, project-level pinning, and transparent download means a fresh checkout on a new machine needs only uv installed — Python itself arrives on demand.

PEP 723 inline scripts

uv has the most complete implementation of PEP 723 inline script metadata in the ecosystem. A standalone Python file declares its dependencies and required interpreter in a header comment, and uv run builds an ephemeral environment for it without any surrounding project structure.

# /// script
# requires-python = ">=3.12"
# dependencies = [
#   "requests<3",
#   "rich",
# ]
# ///

import requests
from rich.pretty import pprint

resp = requests.get("https://peps.python.org/api/peps.json")
pprint([(k, v["title"]) for k, v in resp.json().items()][:10])

Run it with uv run script.py. uv parses the header, downloads any missing Python interpreter, builds an isolated environment, caches it, and executes — typically in well under a second on a warm cache. Inside a project directory the script’s own metadata fully overrides the project deps, so a script can declare an entirely different stack from its host project without contamination. uv extends the PEP with a [tool.uv] table inside the script header that supports exclude-newer for time-bounded reproducible resolution, alternative index sources, and other uv-specific knobs. The practical consequence is that the long-standing dichotomy between shebang scripts (which cannot declare deps) and full projects (which require a directory and a venv) collapses: a single .py file is now a self-contained, reproducible, distributable unit. This replaces both ad-hoc requirements.txt-next-to-script patterns and gist-style one-file tools, and it pairs naturally with uvx for repos that want to ship runnable CLIs without packaging them as wheels.

How uv differs from Poetry, PDM, and Rye

Against Poetry, uv occupies the same scope (project + lockfile + venv + publish) but is materially faster, uses standardised PEP 621 [project] metadata and PEP 735 [dependency-groups] instead of Poetry’s vendor-specific [tool.poetry] namespace, and bundles Python interpreter management. Lockfiles are not interchangeable: poetry.lock and uv.lock use distinct schemas and resolvers. uv supports importing from Poetry projects, and uv add -r accepts existing requirement files, but the lockfile itself is a one-way migration. Against PDM, uv is the closest peer — both are PEP 621-native, both maintain lockfiles, and both target project-style installs — but uv is faster, ships a more complete pipx replacement (uvx), and bundles Python downloads. PDM has a longer track record with PEP 582 (__pypackages__); uv does not adopt that layout. Against Rye, the lineage is direct: Rye was Armin Ronacher’s experiment in unifying the toolchain, and in early 2024 Astral took stewardship and announced Rye would converge on uv. Rye now uses uv internally as its resolver and installer, and new projects are steered to uv directly — treat Rye as legacy and uv as its supported successor. Against the unbundled stack of pip + pip-tools + virtualenv + pipx + pyenv, uv is most clearly designed as a wholesale replacement; the trade-off is the centralisation already noted, in exchange for a single binary, a single lock format, a single cache, and substantially faster everything.

Caveats and trade-offs

uv.lock is uv-specific and not consumable by other tools. For interop with downstream pip-based systems, the supported path is uv pip compile -o requirements.txt pyproject.toml or uv export. This is fine for greenfield projects but occasionally awkward for shared toolchains where multiple resolvers must agree. The pip shim is not a project manager and will install into the wrong place if --system, the active venv, or --python are forgotten — mixing uv pip install with project uv add is a reliable recipe for .venv/ drifting from uv.lock. Hardlink behaviour can warn or fall back on Docker bind mounts, network filesystems, or tmpfs where hardlinks span boundaries oddly; setting UV_LINK_MODE=copy produces predictable behaviour at the cost of disk, and is the recommended setting for CI cache mounts. Conda interoperability is partial: uv handles PyPI-style wheels and sdists but does not resolve against conda channels or handle non-Python conda packages, so mixed scientific-Python environments still need conda or pixi alongside. The bundled uv_build build backend is a fast pure-Python option, but it does not yet handle compiled extensions — projects with C, Rust, or Fortran code still need setuptools, hatchling, or maturin. The plug-in ecosystem is younger than Poetry’s, with fewer third-party exporters and hooks, though the surface area is expanding. Finally, uv self update works only for installations from the standalone installer; uv installed via Homebrew, pip, pipx, winget, or scoop must be updated through that channel. None of these are blocking for the typical workflow, but each is worth knowing before adopting uv as the sole entry point to a Python toolchain.

Local graph