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
uvand 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 auv.lockfile. - 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 pipinterface 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 | shOn 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 uvVerify the installation and note how to keep it current:
uv --version
uv self updateTip: uv installs into
~/.local/binby default. Ifuv --versionis not found, add that directory to yourPATHand 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-cliThis 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 richYou'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 richFor packages with optional features (extras), use --extra:
uv add fastapi --extra standardStep 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 mypyYour 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-materialWhen you deploy, install only what production needs:
uv sync --no-devWhy 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 lockTo bring your environment in sync with the lockfile (the command you'll run most in CI):
uv syncTo upgrade a single package within its constraints:
uv lock --upgrade-package httpxTo upgrade everything:
uv lock --upgradeIn CI you want to fail loudly if the lockfile is stale rather than silently regenerate it:
uv sync --lockedThat 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 checkBecause 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.pyYou can even add a dependency to a standalone script's metadata block automatically:
uv add --script fetch_repos.py pandasStep 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.13Pin a project to an interpreter — this writes .python-version:
uv python pin 3.12When 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 --allEach 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 buildArtifacts land in dist/. Publish them to PyPI (or a private index) with:
uv publishUse 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 venvFor 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 treeIf 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 --lockedto 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.