One tool to replace pip, pip-tools, pipx, poetry, pyenv, virtualenv, twine, and more — 10–100× faster than the originals.
uv pip commanduv venvuv initpyproject.toml, uv add, uv removeuv.lock fileuv sync — install & reconcileuv run — auto-sync & executeuv pythonuvx for one-shot CLI toolsrequirements.txt / Poetryuv is a single Rust-based binary that handles every step of the Python packaging lifecycle — installing the interpreter, creating environments, resolving and locking dependencies, running scripts, building wheels, and publishing them.
ruffpip-compile, pip-syncpython -m venvbuild — build & publish to PyPIThe pitch: "the Cargo of Python" — one fast tool, one config file, one lockfile, no plugin sprawl.
| Operation | pip | uv |
|---|---|---|
Resolve transformers + extras | ~30 s | ~0.4 s |
| Install Django + tree | ~7 s | ~0.2 s |
poetry lock on a medium project | ~25 s | ~0.3 s |
| Re-create venv from cache | ~3 s | ~50 ms |
Measured by Astral; varies by network. 10–100× over pip is the rule, not the exception.
lnuv is distributed as a single static binary. No Python required — uv can install Python for you.
# macOS / Linux
curl -LsSf https://astral.sh/uv/install.sh | sh
# Windows (PowerShell)
powershell -ExecutionPolicy ByPass -c "irm https://astral.sh/uv/install.ps1 | iex"
# pin a version
curl -LsSf https://astral.sh/uv/0.5.0/install.sh | sh
brew install uv # macOS
winget install astral-sh.uv
pacman -S uv # Arch
pipx install uv # if pipx already there
pip install uv # last resort
# Astral's slim image
FROM ghcr.io/astral-sh/uv:0.5-python3.12-bookworm-slim
# or copy the binary
COPY --from=ghcr.io/astral-sh/uv:latest \
/uv /uvx /usr/local/bin/
uv self update
uv self update --version 0.5.4
uv --version
$ uv --version
uv 0.5.4
$ uv python list
cpython-3.13.0-linux-x86_64-gnu
cpython-3.12.7-linux-x86_64-gnu
cpython-3.11.10-linux-x86_64-gnu
...
$ uv pip list
Package Version
---------- -------
(empty system pip-compatible view)
Install via the standalone installer in CI — it does not depend on a working Python. Pin the version in production: UV_VERSION=0.5.4.
uv pip mirrors the pip CLI surface. Faster, no behavioural changes — switch in seconds for legacy projects.
# classic pip
pip install requests
pip install -r requirements.txt
pip freeze > requirements.txt
pip install -e .
pip uninstall flask
# uv equivalent — same flags
uv pip install requests
uv pip install -r requirements.txt
uv pip freeze > requirements.txt
uv pip install -e .
uv pip uninstall flask
# produce a fully-pinned requirements file
uv pip compile requirements.in -o requirements.txt
# sync the venv to exactly match
uv pip sync requirements.txt
# add hashes for supply-chain integrity
uv pip compile requirements.in \
--generate-hashes -o requirements.txt
uv pip vs uv addpyproject.toml required — perfect for legacy requirements.txt repos and one-off venvs.pyproject.toml and updates uv.lock.uv pip as the on-ramp. Migrate to uv add when you want a lockfile.uv pip install \
--index-url https://pypi.org/simple \
--extra-index-url https://internal/index \
--python /usr/bin/python3.11 \
--system \
--no-cache \
--offline \
package==1.2.*
# defaults to .venv
uv venv
# pick a version
uv venv --python 3.12
uv venv --python 3.12.4
uv venv --python pypy@3.10
# pick a path
uv venv ~/envs/scratch
# activate (still POSIX as ever)
source .venv/bin/activate
.\.venv\Scripts\activate # Windows
Every uv pip, uv run, uv add auto-discovers .venv in the current directory. The only reason to source is for ad-hoc python / pytest calls in your shell.
python -m venv: ~3–5 s (copies CPython, runs ensurepip)uv venv: ~50 ms — uses managed Python and skips ensurepip# by default the venv has no pip!
uv venv
# add the legacy seed if you really need it
uv venv --seed # installs pip, setuptools, wheel
uv installs straight into site-packages, so pip-in-venv is mostly vestigial. Only enable seeding when external tools insist on calling pip.
$ uv init my-app
Initialized project `my-app` at /tmp/my-app
$ tree -a my-app
my-app/
├── .gitignore
├── .python-version # pinned Python (e.g. 3.12)
├── README.md
├── main.py # hello-world entry
└── pyproject.toml
# init in-place
uv init --name my-app .
# library scaffold (with src/)
uv init --lib mylib
# application scaffold (default)
uv init --app my-cli
# packaged (build-system + entry points)
uv init --package my-tool
src/ layout, Hatch build system, ready for PyPIcd my-app
uv add requests # creates .venv + uv.lock
uv run main.py # runs in synced env
uv run python -V # 3.12.x
.python-versionA plain text file naming the required Python. Honoured by uv, pyenv, and most IDEs. Commit it.
[project]
name = "my-app"
version = "0.1.0"
description = "Example service"
readme = "README.md"
requires-python = ">=3.11"
authors = [
{ name = "Brendan Lynskey",
email = "you@example.com" }
]
dependencies = [
"fastapi>=0.115",
"httpx>=0.27",
"pydantic>=2.8",
]
[project.optional-dependencies]
postgres = ["asyncpg>=0.29"]
[dependency-groups]
dev = [
"pytest>=8",
"ruff>=0.6",
"mypy>=1.10",
]
[tool.uv]
package = true # install in editable mode
dependencies vs groups vs extraspip install my-app[postgres]| Spec | Means |
|---|---|
requests | any version |
requests==2.31 | exactly |
requests~=2.31 | compatible (≥ 2.31, < 3.0) |
requests>=2.0,<3 | range |
requests; sys_platform != "win32" | marker |
The everyday verbs. Each call edits pyproject.toml, refreshes uv.lock, and syncs the venv — atomically.
# runtime deps
uv add fastapi httpx
# version pin
uv add 'pydantic>=2.8,<3'
# dev-group dependency
uv add --group dev pytest ruff mypy
# extras (optional dep)
uv add --optional postgres asyncpg
# from a local path / git / URL
uv add ./libs/internal-utils
uv add 'git+https://github.com/foo/bar@main'
uv add 'https://files.pythonhosted.org/packages/.../wheel.whl'
# editable
uv add --editable ./libs/internal-utils
# remove
uv remove httpx
uv remove --group dev pytest
# upgrade everything within constraints
uv lock --upgrade
# upgrade just one package
uv lock --upgrade-package fastapi
# upgrade across a major boundary by editing
# pyproject.toml then re-locking
sed -i 's/pydantic>=2.8/pydantic>=3/' pyproject.toml
uv lock
pyproject.toml — adds the dependency line in the right tableuv.lock — re-resolves; the diff is your audit trail.venv — installed/removed via hardlinks from cacheCommit pyproject.toml and uv.lock together. Reviewers can see both intent and effect in one diff.
uv.lock is a universal, cross-platform, multi-Python lockfile. One file pins your project for Linux x86, Linux arm64, macOS arm64, Windows, and every supported Python version simultaneously.
version = 1
requires-python = ">=3.11"
[[package]]
name = "fastapi"
version = "0.115.4"
source = { registry = "https://pypi.org/simple" }
dependencies = [
{ name = "pydantic" },
{ name = "starlette" },
]
sdist = { url = "...", hash = "sha256:abc..." }
wheels = [
{ url = "...py3-none-any.whl",
hash = "sha256:def..." },
]
[[package]]
name = "pydantic"
version = "2.9.2"
...
# refuse to update the lock — fail if it's out of date
uv sync --frozen
uv sync --locked # alias
# ignore lock and re-resolve from pyproject
uv sync --no-lock
Always run uv sync --frozen. If the lockfile is stale the build fails immediately — the way it should.
uv.lock matches pyproject.toml (re-lock if not).venv exists with the right Python.venv matches the lock exactlyIt is idempotent. Running it twice is essentially free.
uv sync # default groups
uv sync --all-groups # everything dev + extras
uv sync --no-dev # production install
uv sync --extra postgres # one extra
uv sync --group docs # one dev group
uv sync --frozen # CI / reproducible
uv sync --reinstall # nuke and rebuild
uv sync --reinstall-package torch
$ time uv sync # cold
real 0m1.245s
$ time uv sync # warm
real 0m0.034s # nothing to do
Because uv hashes the lock + venv state, a clean sync becomes a 30 ms no-op. CI pipelines can uv sync at the start of every job without paying for it.
# inside a Docker stage
ENV UV_COMPILE_BYTECODE=1 \
UV_LINK_MODE=copy
RUN uv sync --frozen --no-dev
--no-dev drops dev groupsUV_COMPILE_BYTECODE precompiles .pycUV_LINK_MODE=copy avoids hardlink issues across volumesuv run auto-syncs the environment, then exec's the command inside it. No activation, no poetry shell.
uv run python main.py
uv run pytest -q
uv run ruff check .
uv run mypy src/
uv run ipython
uv run python -c 'import torch; print(torch.__version__)'
# with extra runtime deps not in the project
uv run --with rich --with httpx python -c \
'from rich import print; print({"hi": 1})'
# pin a Python different from .python-version
uv run --python 3.13 pytest
Define entry points in pyproject.toml, then uv run calls them by name.
[project.scripts]
serve = "myapp.cli:main"
# now
uv run serve
--no-syncVIRTUAL_ENV, PATH, PYTHONPATH for the child process--frozen is setuv run python is not the same as a previously-activated Python. If you've manually source'd the venv, drop the uv run prefix to avoid double activation.
uv ships its own Python distribution manager — replacing pyenv, deadsnakes, and the official installers.
uv python list # installed + downloadable
uv python list --only-installed
uv python install 3.12.7
uv python install 3.11 3.12 3.13
uv python install 'cpython-3.12.7-linux-x86_64-gnu'
uv python install pypy@3.10
# uninstall
uv python uninstall 3.11
# show where one lives
uv python find 3.12
# write/update .python-version
uv python pin 3.12
# uses the project's .python-version
uv venv
# override per command
uv run --python 3.13 pytest
~/.local/share/uv/python/ and shared across every projectapt install python3.12--system opts back to the OS interpreter (rare; mostly Docker)UV_PYTHON_PREFERENCE=only-system if your security policy forbids downloading interpretersSingle-file scripts can declare their dependencies inside the script. uv reads the metadata, builds an ephemeral venv, runs the script.
#!/usr/bin/env -S uv run --script
# /// script
# requires-python = ">=3.12"
# dependencies = [
# "httpx>=0.27",
# "rich>=13",
# ]
# ///
import httpx
from rich import print
r = httpx.get("https://api.github.com/zen")
print(f"[bold green]GitHub says:[/] {r.text}")
$ chmod +x zen.py
$ ./zen.py
GitHub says: Practicality beats purity.
pyproject.toml, no venv, no requirements.txt# add a dep to a script's PEP 723 block
uv add --script zen.py "click>=8"
# pin Python
uv add --script zen.py --python 3.13
# lock script deps separately (optional)
uv lock --script zen.py
PEP 723 is an accepted Python standard. Hatch, pdm, and pipx implement it too — your script stays portable.
# run a CLI without installing it
uvx ruff check .
uvx black .
uvx pre-commit run --all-files
uvx httpie GET https://example.com
# specific version
uvx ruff@0.6.9 check .
# from git
uvx --from 'git+https://github.com/foo/bar' baz
# pass extras / extra deps
uvx --with pandas streamlit run app.py
uv tool install ruff
uv tool install ruff@0.6.9
uv tool install pre-commit --with pre-commit-hooks
uv tool list
uv tool upgrade --all
uv tool uninstall ruff
~/.local/share/uv/tools/~/.local/bin (configurable via UV_TOOL_BIN_DIR)PATH with: uv tool update-shelluvx vs uv tool install| uvx | uv tool install | |
|---|---|---|
| Install location | tmp / cache | persistent |
| On PATH | no | yes |
| Best for | occasional use | daily drivers |
Use uvx in CI pre-commit jobs. It avoids polluting the build venv and is faster than pip install --no-deps pre-commit.
A workspace is a multi-package repo with one shared lockfile, one shared venv, and one resolution pass. Mirrors Cargo / pnpm workspaces.
repo/
├── pyproject.toml # workspace root
├── uv.lock # ONE lockfile
├── apps/
│ ├── api/
│ │ └── pyproject.toml
│ └── worker/
│ └── pyproject.toml
└── libs/
├── core/
│ └── pyproject.toml
└── adapters/
└── pyproject.toml
pyproject.toml[tool.uv.workspace]
members = ["apps/*", "libs/*"]
exclude = ["apps/legacy"]
[tool.uv.sources]
core = { workspace = true }
adapters = { workspace = true }
# apps/api/pyproject.toml
[project]
name = "api"
dependencies = [
"core", # path-resolved via workspace
"adapters",
"fastapi>=0.115",
]
[tool.uv.sources]
core = { workspace = true }
adapters = { workspace = true }
# sync everything
uv sync
# scope to a member
uv sync --package api
uv run --package api uvicorn api.main:app
# add a dep to one member
uv add --package worker celery
Editable cross-package deps without manual pip install -e juggling. Refactor across apps and libs with one resolve.
Internal-only groups for dev tooling, docs, profiling — never installed by your users.
[dependency-groups]
dev = ["pytest>=8", "ruff>=0.6"]
docs = ["mkdocs>=1.6", "mkdocs-material"]
profile = ["py-spy", "memray"]
typing = ["mypy>=1.10",
"types-requests"]
# include another group inside one
test = [{ include-group = "dev" },
"pytest-asyncio"]
uv sync --group dev
uv sync --group dev --group typing
uv sync --all-groups
uv run --group test pytest
Public flags your users opt into when installing your package.
[project.optional-dependencies]
postgres = ["asyncpg>=0.29"]
redis = ["redis>=5"]
all = ["my-app[postgres,redis]"]
# consumer
pip install my-app[postgres]
uv pip install 'my-app[postgres,redis]'
| Groups | Extras | |
|---|---|---|
| Visible to users | no | yes |
| Use case | dev tooling | opt-in features |
| Standard | PEP 735 | PEP 621 |
| Cmd | uv sync --group X | uv sync --extra X |
# build into ./dist/
uv build
# only one format
uv build --wheel
uv build --sdist
# specify a workspace member
uv build --package my-lib
# inspect
unzip -l dist/my_lib-0.1.0-py3-none-any.whl
Replaces python -m build. Reads the build-system table from pyproject.toml — Hatchling, setuptools, maturin (Rust extensions), all work.
uv publish # uses ~/.pypirc
uv publish --token "$PYPI_TOKEN"
uv publish --publish-url https://test.pypi.org/legacy/
Replaces twine. Supports trusted publishing (OIDC) from GitHub Actions — no token to manage.
name: Publish
on:
release:
types: [published]
permissions:
id-token: write # OIDC for PyPI
jobs:
pypi:
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v4
- uses: astral-sh/setup-uv@v3
- run: uv build
- run: uv publish --trusted-publishing automatic
uv version 1.2.0
uv version --bump patch
uv version --bump minor
uv version --bump major
Edits pyproject.toml & refreshes the lock — commit the diff and tag.
~/.cache/uv/ (Linux/macOS) or %LOCALAPPDATA%\uv\cacheuv cache dir, uv cache prune, uv cache cleanUV_LINK_MODE=hardlink — venv files are filesystem links into the cache--link-mode=reflink (copy-on-write)--link-mode=copy (Docker volumes, NFS)--link-mode=symlinkUV_CACHE_DIR=/big/disk/uv-cache
UV_LINK_MODE=copy
UV_HTTP_TIMEOUT=120
UV_CONCURRENT_DOWNLOADS=20
UV_NO_CACHE=1
UV_OFFLINE=1
UV_INDEX_URL=https://internal/simple
UV_NATIVE_TLS=1
| Capability | pip | Poetry | pdm | Hatch | uv |
|---|---|---|---|---|---|
| Install / uninstall | ✅ | ✅ | ✅ | ✅ | ✅ |
| Lockfile | — | ✅ | ✅ | — | ✅ |
| Universal lock | — | partial | ✅ | — | ✅ |
| Workspaces | — | — | partial | — | ✅ |
| Manage Python | — | — | partial | ✅ | ✅ |
| Build / publish | — | ✅ | ✅ | ✅ | ✅ |
| PEP 723 scripts | — | — | partial | ✅ | ✅ |
| Run CLIs (pipx) | — | — | — | — | ✅ |
| Single binary | — | — | — | — | ✅ |
| Resolver speed | slow | slow | medium | medium | very fast |
pyproject.toml — use uv pip onlyUV_OFFLINE=1# nothing to change in the repo, just:
alias pip='uv pip'
uv pip install -r requirements.txt
uv pip compile requirements.in -o requirements.txt
uv pip sync requirements.txt
Useful for proving uv works in your CI before any rewrite.
# interactive, infers from cwd
uv init --name my-app .
# import the existing requirements
xargs -a requirements.txt uv add
uv lock
git add pyproject.toml uv.lock
git rm requirements.txt requirements.in
# CI now does
uv sync --frozen --no-dev
requirements.txt for downstream consumers# export from the lock for tools that
# can't read uv.lock yet
uv export --format requirements-txt \
--no-dev --no-hashes \
-o requirements.txt
Useful for AWS Lambda, legacy Docker base images, IDE remote interpreters, vendor security scanners.
Don't keep both requirements.txt and uv.lock as sources of truth. Pick one. Export from the lock; never edit a generated requirements file by hand.
| Poetry | uv |
|---|---|
poetry install | uv sync |
poetry add X | uv add X |
poetry remove X | uv remove X |
poetry lock | uv lock |
poetry run X | uv run X |
poetry shell | source .venv/bin/activate |
poetry build / publish | uv build / uv publish |
[tool.poetry] | [project] + [tool.uv] |
poetry.lock | uv.lock |
[tool.poetry] rewrite# before
[tool.poetry]
name = "svc"
version = "1.2.3"
authors = ["You <you@x.com>"]
[tool.poetry.dependencies]
python = "^3.12"
fastapi = "^0.115"
[tool.poetry.group.dev.dependencies]
pytest = "^8"
# after
[project]
name = "svc"
version = "1.2.3"
requires-python = ">=3.12,<3.14"
authors = [{name = "You",
email = "you@x.com"}]
dependencies = ["fastapi>=0.115,<1"]
[dependency-groups]
dev = ["pytest>=8"]
^1.2.3 → uv's >=1.2.3,<2~1.2.3 → uv's ~=1.2.3 (compatible release)poetry-plugin-export → uv export --format requirements-txt[tool.uv]
# install the project itself in editable mode
package = true
# always sync these groups by default
default-groups = ["dev"]
# constrain Python versions for the lock
python-preference = "managed"
required-version = ">=0.5"
# index configuration
index-strategy = "unsafe-best-match"
prerelease = "if-necessary"
# resolve only certain platforms
[tool.uv.environments]
linux-x86 = "platform_system == 'Linux' and platform_machine == 'x86_64'"
# private indexes
[[tool.uv.index]]
name = "internal"
url = "https://pkg.acme.com/simple"
priority = "supplemental"
explicit = true
[tool.uv.sources]
mylib = { index = "internal" }
uv.tomlSame keys, no [tool.uv] prefix. Lives at ~/.config/uv/uv.toml:
cache-dir = "/big/disk/uv-cache"
link-mode = "copy"
python-preference = "only-managed"
[[index]]
name = "company"
url = "https://pkg.acme.com/simple"
UV_*)pyproject.toml [tool.uv]~/.config/uv/uv.tomlexport UV_LINK_MODE=copy
export UV_PYTHON_PREFERENCE=only-managed
export UV_TOOL_BIN_DIR="$HOME/.local/bin"
name: CI
on: [push, pull_request]
jobs:
test:
runs-on: ubuntu-latest
strategy:
matrix:
python: ["3.11", "3.12", "3.13"]
steps:
- uses: actions/checkout@v4
- uses: astral-sh/setup-uv@v3
with:
version: "0.5.4"
enable-cache: true
cache-dependency-glob: "uv.lock"
- run: uv python install ${{ matrix.python }}
- run: uv sync --frozen --all-groups
- run: uv run ruff check .
- run: uv run mypy src/
- run: uv run pytest -q --cov
setup-uv@v3~/.cache/uv keyed on uv.lockuv & uvx to PATHUV_COMPILE_BYTECODE=1 — pre-compile .pyc at install time, faster cold start in testsuv sync --frozen instead of uv lock && uv syncactions/setup-python entirely; uv python install is faster- run: uv lock --check
# fails if pyproject.toml changed
# without re-locking
| Task | Command |
|---|---|
| Start a project | uv init my-app |
| Add a dep | uv add httpx |
| Add dev dep | uv add --group dev pytest |
| Remove dep | uv remove httpx |
| Sync env | uv sync |
| Run a script | uv run python main.py |
| Run tests | uv run pytest |
| Run a one-off CLI | uvx ruff check . |
| Install a CLI globally | uv tool install pre-commit |
| Pin a Python | uv python pin 3.12 |
| Re-lock w/ upgrade | uv lock --upgrade |
| Task | Command |
|---|---|
| Reproducible install | uv sync --frozen |
| Production install | uv sync --frozen --no-dev |
| Build wheels | uv build |
| Publish to PyPI | uv publish |
| Bump version | uv version --bump patch |
| Export to req.txt | uv export -o req.txt |
| Inspect dep tree | uv tree |
| Why is X here? | uv tree --invert --package X |
| Outdated deps | uv tree --outdated |
| Cache dir | uv cache dir |
| Clean cache | uv cache prune |
Docker bind mounts and NFS often refuse hardlinks: error: Failed to create hard link.
# fix
export UV_LINK_MODE=copy
Lock was made for >=3.12 but CI is on 3.11. Either bump CI Python (preferred) or relax requires-python.
[[tool.uv.index]]
name = "internal"
url = "https://pkg.acme.com/simple"
# in CI
UV_INDEX_INTERNAL_USERNAME=ci
UV_INDEX_INTERNAL_PASSWORD=$TOKEN
uv reads UV_INDEX_<NAME>_USERNAME / _PASSWORD.
[tool.uv.sources]
torch = [
{ index = "pytorch-cu124",
marker = "platform_system != 'Darwin'" },
{ index = "pypi",
marker = "platform_system == 'Darwin'" },
]
[[tool.uv.index]]
name = "pytorch-cu124"
url = "https://download.pytorch.org/whl/cu124"
explicit = true
uv cache dir
uv cache prune # safe — keeps recent
uv cache clean # nuke everything
If you want import myapp to work after uv sync, add:
[tool.uv]
package = true
uv pip as the drop-in path; uv add as the project pathuv.lockuv sync & uv run for everyday workuvx toolsdocs.astral.sh/uv · github.com/astral-sh/uv · python-build-standalone — github.com/indygreg/python-build-standalone · PEP 723 — peps.python.org/pep-0723 · PEP 735 — peps.python.org/pep-0735
uv pip against an existing projectuv init . + uv addactions/setup-python with astral-sh/setup-uvruff, pre-commit) under uv tool install[tool.uv.workspace]For Docker, multi-stage builds, GPU/PyTorch recipes, monorepo workflows, and migration playbooks see "uv in Practice".
If you're still typing python -m venv, pip install -r, or poetry install — try uv sync once. You won't go back.