writing/tutorial/2026/06
TutorialJun 6, 2026·24 min read

uv: The Complete Guide to the Fast Python Package Manager (2026)

Master uv — the Rust-powered Python package and project manager that replaces pip, pip-tools, pipx, poetry, and pyenv with 10-100x faster performance. This guide covers installation, project setup, dependency groups, lockfiles, tools, Python version management, workspaces, and Docker.

Prerequisites

Before starting this guide, ensure you have:

  • A terminal (macOS, Linux, or Windows with PowerShell)
  • Basic familiarity with the command line
  • Some prior exposure to Python and pip (helpful but not required)
  • No existing Python installation needed — uv can manage Python for you

What You'll Learn

By the end of this guide you'll be able to:

  • Install uv and understand why it replaces a whole stack of tools
  • Create and structure a Python project with a pyproject.toml
  • Add, remove, and pin dependencies with a reproducible lockfile
  • Manage Python versions without pyenv
  • Run command-line tools without polluting your global environment
  • Organize multi-package monorepos with workspaces
  • Build a fast, cached Docker image for production

Why uv?

For more than a decade, Python developers juggled a confusing toolbox: pip to install, pip-tools or poetry to lock, virtualenv to isolate, pyenv to switch Python versions, and pipx to run global tools. Each tool had its own config, its own quirks, and its own performance ceiling.

uv, built in Rust by Astral (the team behind the Ruff linter), collapses that entire stack into a single binary. It is frequently 10 to 100 times faster than pip-based workflows because it parallelizes downloads, caches aggressively with a global content-addressed store, and resolves dependencies with a purpose-built resolver.

The headline benefits:

  • One tool, one config. Projects live in a standard pyproject.toml, locked by a uv.lock file.
  • Speed. Cold installs feel instant; warm installs are near-zero thanks to hard-linked cache reuse.
  • Python management built in. uv downloads and pins Python interpreters itself — no system Python required.
  • Drop-in pip compatibility. A uv pip interface lets you migrate gradually.

Step 1: Install uv

uv ships as a standalone binary with no Python prerequisite.

On macOS or Linux:

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

On Windows (PowerShell):

powershell -ExecutionPolicy ByPass -c "irm https://astral.sh/uv/install.ps1 | iex"

You can also install it through Homebrew, pipx, or even pip:

brew install uv
# or
pipx install uv

Verify the installation and note how to keep it current:

uv --version
uv self update

Tip: uv installs into ~/.local/bin by default. If uv --version is not found, add that directory to your PATH and restart your shell.

Step 2: Create Your First Project

Let's scaffold a project. uv supports several layouts — --app for applications, --lib for distributable libraries, and --package for packaged apps with a build backend.

uv init weather-cli --app
cd weather-cli

This generates a clean starting point:

weather-cli/
├── .python-version
├── README.md
├── main.py
└── pyproject.toml

The pyproject.toml is the single source of truth:

[project]
name = "weather-cli"
version = "0.1.0"
description = "Add your description here"
readme = "README.md"
requires-python = ">=3.12"
dependencies = []

Notice there is no virtual environment yet and no Python install step. uv creates both lazily the first time you add a dependency or run code.

Step 3: Add and Remove Dependencies

Adding a package does three things at once: it updates pyproject.toml, creates a .venv if needed, and writes a lockfile.

uv add httpx rich

You'll see uv resolve, download, and install in a fraction of a second:

Using CPython 3.12.4
Creating virtual environment at: .venv
Resolved 8 packages in 12ms
Installed 8 packages in 23ms
 + httpx==0.27.2
 + rich==13.9.4
 ...

Need a specific version range? Pass a constraint directly:

uv add "httpx>=0.27,<1.0"

Removing is symmetric and also updates the lockfile:

uv remove rich

For packages with optional features (extras), use --extra:

uv add fastapi --extra standard

Step 4: Dependency Groups for Dev Tools

Production dependencies and development dependencies should not mix. uv uses the standard [dependency-groups] table, with a built-in dev group.

Add tools that only matter while developing:

uv add --dev pytest ruff mypy

Your pyproject.toml now records them separately:

[dependency-groups]
dev = [
    "pytest>=8.3.0",
    "ruff>=0.8.0",
    "mypy>=1.13.0",
]

You can define arbitrary named groups too — for example a docs group:

uv add --group docs mkdocs mkdocs-material

When you deploy, install only what production needs:

uv sync --no-dev

Why this matters: keeping linters, type checkers, and test runners out of your production image makes builds smaller, faster, and more secure.

Step 5: Lockfiles and Reproducibility

Every time dependencies change, uv updates uv.lock — a fully resolved, cross-platform, hashed snapshot of your entire dependency tree. Commit it to version control.

To regenerate the lockfile explicitly:

uv lock

To bring your environment in sync with the lockfile (the command you'll run most in CI):

uv sync

To upgrade a single package within its constraints:

uv lock --upgrade-package httpx

To upgrade everything:

uv lock --upgrade

In CI you want to fail loudly if the lockfile is stale rather than silently regenerate it:

uv sync --locked

That flag asserts the lockfile is already up to date — perfect for reproducible pipelines.

Step 6: Running Code and Scripts

Never activate a virtual environment again. uv run executes a command inside the project environment, syncing dependencies first if needed.

uv run main.py
uv run pytest
uv run ruff check

Because uv guarantees the environment matches the lockfile before running, "works on my machine" bugs largely disappear.

uv also supports inline script dependencies — a single-file script can declare its own dependencies using the PEP 723 standard:

# /// script
# requires-python = ">=3.12"
# dependencies = ["httpx", "rich"]
# ///
 
import httpx
from rich import print
 
resp = httpx.get("https://api.github.com")
print(resp.json())

Run it with zero setup — uv creates an ephemeral environment on the fly:

uv run fetch_repos.py

You can even add a dependency to a standalone script's metadata block automatically:

uv add --script fetch_repos.py pandas

Step 7: Managing Python Versions

uv replaces pyenv entirely. It can download and switch between CPython builds without touching your system Python.

List what's available and install specific versions:

uv python list
uv python install 3.11 3.12 3.13

Pin a project to an interpreter — this writes .python-version:

uv python pin 3.12

When you run uv sync or uv run, uv automatically uses (and if necessary downloads) the pinned interpreter. New contributors clone the repo and get the exact Python version with no manual setup.

Step 8: Running Tools with uvx

For one-off command-line tools you don't want in your project — formatters, scaffolders, HTTP clients — uv provides an isolated runner. uvx is shorthand for uv tool run.

Run a tool in a throwaway environment:

uvx ruff check .
uvx cowsay -t "uv is fast"

If you want a tool available permanently on your PATH (the pipx use case), install it:

uv tool install ruff
uv tool list
uv tool upgrade --all

Each tool gets its own isolated environment, so their dependencies never conflict with each other or with your projects.

Step 9: Workspaces for Monorepos

Large codebases often contain several related packages. uv workspaces, inspired by Cargo, let multiple packages share a single lockfile and virtual environment while remaining independently buildable.

Define the workspace in the root pyproject.toml:

[tool.uv.workspace]
members = ["packages/*"]

Then have one package depend on another using a workspace source:

[project]
dependencies = ["shared-core"]
 
[tool.uv.sources]
shared-core = { workspace = true }

Now uv sync resolves the entire workspace together, and a change in shared-core is immediately visible to every package that depends on it — no publishing, no reinstalling.

For dependencies that come from Git or a local path instead, [tool.uv.sources] handles those too:

[tool.uv.sources]
my-lib = { git = "https://github.com/acme/my-lib", tag = "v1.2.0" }
local-tool = { path = "../local-tool", editable = true }

Step 10: Building and Publishing

When your library is ready to share, uv builds standard wheel and source distributions:

uv build

Artifacts land in dist/. Publish them to PyPI (or a private index) with:

uv publish

Use a token via the UV_PUBLISH_TOKEN environment variable rather than hardcoding credentials.

Step 11: A Production Docker Image

uv shines in containers thanks to its cache mounts and lockfile-driven installs. The pattern below installs dependencies in a cached layer before copying your source, so code changes don't bust the dependency cache.

FROM python:3.12-slim
 
# Copy the uv binary from the official image
COPY --from=ghcr.io/astral-sh/uv:latest /uv /uvx /bin/
 
WORKDIR /app
 
# Install dependencies first, using a cache mount and bind mounts
RUN --mount=type=cache,target=/root/.cache/uv \
    --mount=type=bind,source=uv.lock,target=uv.lock \
    --mount=type=bind,source=pyproject.toml,target=pyproject.toml \
    uv sync --locked --no-install-project --no-dev
 
# Now copy the project and install it
COPY . /app
RUN --mount=type=cache,target=/root/.cache/uv \
    uv sync --locked --no-dev
 
# Run inside the synced environment
CMD ["uv", "run", "main.py"]

The two-stage sync — dependencies first, then the project — means that as long as uv.lock and pyproject.toml are unchanged, Docker reuses the heavy dependency layer and only rebuilds the tiny project layer.

Migrating from pip or Poetry

You don't have to rewrite everything at once. uv ships a pip-compatible interface:

uv pip install -r requirements.txt
uv pip compile requirements.in -o requirements.txt
uv venv

For a full migration, run uv init in an existing project, move your requirements.txt entries into pyproject.toml with uv add, and commit the generated uv.lock. Poetry projects already use pyproject.toml, so the main task is translating the [tool.poetry.dependencies] section into the standard [project] dependencies table and running uv lock.

Testing Your Setup

Verify the whole workflow end to end:

uv init demo --app && cd demo
uv add httpx
uv add --dev pytest
uv run python -c "import httpx; print(httpx.__version__)"
uv sync --locked
uv tree

If uv tree prints your dependency graph and uv sync --locked exits cleanly, your project is reproducible and ready for CI.

Troubleshooting

uv: command not found after install — the binary directory (~/.local/bin) isn't on your PATH. Add it and restart the shell.

Lockfile keeps changing in CI — you're running uv sync instead of uv sync --locked. Use the locked variant in pipelines so a stale lockfile fails the build instead of being silently rewritten.

Wrong Python version selected — check for a stray .python-version file or a requires-python constraint that excludes your interpreter. Run uv python pin to set the version explicitly.

Slow first install — only the very first run populates the global cache. Subsequent installs across all projects reuse hard-linked files and are dramatically faster.

Next Steps

  • Pair uv with Ruff (also from Astral) for a unified, Rust-powered lint-and-format setup
  • Explore inline-script dependencies for throwaway automation and data scripts
  • Add uv sync --locked to your CI before tests to guarantee reproducible runs
  • Read more Python tooling guides on noqta.tn, including our FastAPI Docker production guide and Pydantic AI type-safe agents

Conclusion

uv collapses the fragmented Python tooling landscape into one fast, coherent tool. With a single binary you scaffold projects, manage dependencies and Python versions, lock for reproducibility, run isolated tools, organize monorepos, and ship lean Docker images. The speed is the hook, but the real win is one mental model instead of five. Install it, run uv init, and you'll rarely reach for pip, poetry, pyenv, or pipx again.