uv (Astral) Operational Guide
Concise operational pointers for uv, Astral's Rust-based packaging and runtime tool — replaces pip / pip-tools / virtualenv / pyenv / poetry / pipx in one binary.
Assumes you already know pip basics, virtual envs, and pyproject.toml. This skill covers the uv-specific layer — project model, lockfile semantics, workspace layout, source resolution, build isolation, managed Python, and migration traps that LLMs gloss over because they look superficially like pip.
When to use
Load when the question is about:
- uv project layout (
pyproject.toml+uv.lock+.python-version) and the[tool.uv]/[tool.uv.sources]/[tool.uv.workspace]/[tool.uv.index]tables - Lockfile behaviour:
uv lock,uv sync --lockedvs--frozen,--upgrade-package, universal resolution, environment markers,requires-python - Dependency groups (PEP 735) vs optional-dependencies vs the legacy
tool.uv.dev-dependencies - Workspaces:
membersglobs, source inheritance, when to prefer path deps - Sources: alternative indexes, git/path/url,
--no-build-isolation,dependency-metadata, wheels-only packages (PyTorch, flash-attn) - Managed Python toolchain:
uv python install/pin,python-preference,requires-pythoninterplay - Tool envs:
uv tool installvsuvx(uv tool run), pipx migration - CI patterns and Docker layering with
UV_LINK_MODE,UV_COMPILE_BYTECODE,--no-install-project - Migration from poetry / pip-tools / pipx (caret versions, dynamic versioning,
requirements.in) - PEP 723 inline-script metadata (
# /// scriptblock),.py.lockper-script
Do NOT load for: generic Python coding, library API questions, asking what a virtual env is, or "how do I install a package" with no uv-specific friction.
Project model and lifecycle commands
- Init flavours (
uv init):--app(default; flatmain.py, no[build-system], not installed into env),--package(addssrc/, build-system, entry points; project IS installed),--lib(implies--package, addspy.typed). Default build backend since uv 0.8 isuv_build(uv_build>=0.11.7,<0.12) — pure-Python only; switch tohatchlingfor VCS versioning, native ext, build hooks. Pre-July-2025 default washatchling. uv add <pkg>editspyproject.tomlAND relocks AND syncs.uv pip install <pkg>does none of those — it's a pip-shim that mutates.venvonly. Mixing the two is the #1 newbie footgun:uv pip installpackages disappear on the nextuv syncbecause they aren't inuv.lock.uv lockre-resolves and writesuv.lockwithout touching.venv. Flags:--upgrade(relax all locked versions),--upgrade-package <pkg>(relax only one — preserves all other pins),--check(CI: fail if relock would change anything).uv lock --script foo.pywritesfoo.py.lockadjacent.uv syncreconciles.venvto matchuv.lock. Default mode is exact: removes anything not in lock. Use--inexactto retain extras. Always installs the project as editable unless--no-install-project(deps only) or--no-install-workspace(workspace members excluded).uv sync --locked= "fail ifuv.lockwould change" (CI gate).uv sync --frozen= "skip the resolver entirely; useuv.lockas-is" — fastest, but errors if lock is missing/stale.uv run <cmd>= auto-sync THEN exec. For one-off invocations preferuv run --frozen <cmd>in CI to skip resolution.uv run --no-projectignorespyproject.toml(pure script mode).uv run --isolatedignores caches/lockfile/sources.uvxis a hard alias foruv tool run. Not the same asuv run.uvxruns in a disposable cache env;uv runuses the project.venv.
Lockfile semantics: universal by default
uv.lockis universal / cross-platform — one lockfile encodes resolutions for every platform/Python combination, gated by PEP 508 markers. This is fundamentally different from pip-tools which produces onerequirements.txtper platform.- The resolution space is bounded by
requires-pythonin[project]AND[tool.uv.environments]. Narrowtool.uv.environments(e.g., drop Windows) when resolution fails because of Windows-only wheels-conflict — common with ML stacks. [tool.uv.required-environments]= "the lock MUST cover these markers". Forces the resolver to fail loudly if a wheel-only dep (e.g. PyTorch CUDA) lacks coverage for a stated platform.- Sources of stale-lock failures:
requires-pythonwidened inpyproject.tomlbut lockfile not regenerated;--frozenwill then fail at sync. Runuv lockto repair. uv.lockformat is uv-private TOML — do not edit by hand. Commit it for apps; for libraries it's still recommended (it locks the dev env, not what consumers see).- Pre-release behaviour: uv requires explicit opt-in via
--prerelease alloweven for transitive deps. Pip accepts pre-releases of transitive deps silently — common surprise on migration.
Dependency groups (PEP 735)
- The standard location is
[dependency-groups](top-level, NOT under[tool.uv]). The legacy[tool.uv.dev-dependencies]is deprecated; uv merges both into thedevgroup during resolution but new code should use[dependency-groups]. - Group flags:
--group <name>,--no-group <name>(exclusion wins),--all-groups,--no-default-groups,--only-group. Thedevgroup is special-cased and synced by default; flags--dev/--no-dev/--only-dev. - Override default groups via
[tool.uv] default-groups = ["dev", "test"]. CI prod install:uv sync --locked --no-default-groups(or setUV_NO_DEV=1and use group-by-group). - Groups can nest:
dev = [{include-group = "test"}, "ipython"]. - Groups are local-only — they do NOT publish to PyPI. Optional dependencies (
[project.optional-dependencies]) DO publish — that's the structural distinction. Use extras for "consumers can opt in", groups for "developer tooling". uv pip install --group <name>was added later; pre-uv 0.5 the pip shim couldn't see groups.
[tool.uv.sources] and alternative indexes
- Five source types: index (named), git (with
tag/branch/rev/subdirectory/lfs), url, path, workspace ({ workspace = true }). - Sources only apply during development — they are stripped on publish (so
git = "..."deps don't leak into PyPI metadata). This is intentional and a common confusion. - Multi-marker sources: provide a list with PEP 508 markers, e.g.
httpx = [{ git = "...", marker = "sys_platform == 'darwin'" }, { index = "internal", marker = "sys_platform == 'linux'" }]. [[tool.uv.index]]defines named indexes:name,url,default = true(only one allowed since uv 0.10),explicit = true(packages from this index require an explicit source mapping — the typical PyTorch pattern).--index-strategy unsafe-best-matchwalks ALL indexes for the highest version. Default isfirst-index(security: no dependency confusion). Required for some PyTorch CUDA setups; understand the supply-chain trade-off before flipping it.- Workspace member references:
mypkg = { workspace = true }. The version comes from the member's ownpyproject.toml, not the root.
Workspaces
- Configured at the root
pyproject.toml:[tool.uv.workspace] members = ["packages/*"], optionalexclude. Every matched dir must containpyproject.toml. - The root is itself a workspace member unless explicitly excluded. One
.venvfor the entire workspace, oneuv.lockat the root. - Source inheritance:
[tool.uv.sources]in the root applies to all members; a member-local[tool.uv.sources]for a given dependency fully overrides the root entry — markers and all are discarded. - All members share
requires-pythonresolution; conflictingrequires-pythonbetween members forces a single intersected version range. - Use a workspace when members iterate together and share deps; use path deps without
[tool.uv.workspace]when members must keep separate venvs / requires-python / conflicting deps. uv sync --package <member>syncs only that member's deps (still uses the workspace lock).uv run --package <member> <cmd>runs in the workspace venv but with that member as the project.
Managed Python toolchain
uv python install 3.13downloads python-build-standalone binaries to${UV_PYTHON_INSTALL_DIR:-~/.local/share/uv/python}. Multiple versions coexist;uv python listshows them.uv python uninstall <ver>.uv python pin 3.13writes.python-version(project-local).uv python pin --global 3.13writes user-config default. Pin is honoured byuv tool install/runsince uv 0.10.- Discovery order: managed pythons →
PATH(python,python3,python3.x) → Windows registry / Microsoft Store. First compatible wins, NOT newest — easy gotcha whenpython3.10shadows a managed3.13. python-preference(alsoUV_PYTHON_PREFERENCE):managed(default; prefer managed but accept system),only-managed(refuse system),system(prefer system),only-system(refuse downloads). Setonly-systemin containers shipping their own CPython to prevent surprise downloads.- Free-threaded / debug builds:
3.13t(free-threaded),3.13d(debug). PyPy / GraalPy / Pyodide also supported — note since uv 0.10 their executables are namedpypy3.10etc., not genericpython3.10. - Auto-download:
UV_PYTHON_DOWNLOADS=neverdisables. Useful in air-gapped CI.
Build isolation and wheels-only deps
- Default: every build runs in a clean isolated env using PEP 517 (correct, but slow for
flash-attn/deepspeed-style packages that importtorchat build time). - Three escape hatches, in order of preference:
extra-build-dependencies(uv 0.10+): augment the isolated env with extra packages. Withmatch-runtime = true, uv injects the runtime version of the package into the build env — solves thetorch-at-build-time problem cleanly.dependency-metadata: declare a package's metadata inpyproject.tomlso the resolver doesn't need to build it for solving — only at install. Use for packages with stable, known metadata that are expensive to build.no-build-isolation-package = ["flash-attn"](or--no-build-isolationglobally): turn off isolation. You MUST thenuv pip install <build-deps>first, or rely on what's in the env. Most fragile path; reach for it last.
[tool.uv.required-environments]for wheels-only packages (PyTorch CUDA): forces the lock to verify wheel availability for declared markers; without it the lock can succeed butuv syncfails on a target machine.- Conflicts (
[tool.uv] conflicts = [[{ extra = "cpu" }, { extra = "cu128" }]]): tells the resolver these are mutually exclusive — required for the CPU/CUDA-extras pattern, otherwise universal resolution will try to satisfy both at once and fail.
Cache, link mode, and bytecode
- Cache lives at
${UV_CACHE_DIR:-${XDG_CACHE_HOME:-~/.cache}/uv}. Cache MUST live on the same filesystem as.venvfor hardlink installs to work — the most common Docker layering bug. UV_LINK_MODEvalues:clone(CoW reflinks; APFS/btrfs/xfs default — fastest, zero-copy),hardlink(Linux ext4 default),copy(slowest, always safe),symlink(rare). In multi-stage Docker builds setUV_LINK_MODE=copyor links break across mounts.UV_COMPILE_BYTECODE=1runscompileallpost-install; doubles install time but cuts cold-start for large apps. Standard in production Docker images, off in dev.uv cache cleannukes everything (oruv cache clean <pkg>).uv cache pruneremoves only unused entries — the routine maintenance command.uv cache prune --ciadditionally drops pre-built wheels but keeps source-built wheels (rebuilding from source is more expensive than re-downloading).
CI patterns
- Canonical CI sync:
uv sync --locked --no-default-groups --group ci(or--no-devif just dev exists).--lockedgates the lockfile — fails ifpyproject.tomldrifted fromuv.lock. Never use--frozenfor the install step in CI unless you are also runninguv lock --checkseparately;--frozensilently uses a stale lock. - Reproducible exec:
uv run --frozen <cmd>AFTER a successfuluv sync --locked— skips re-resolution per command. - GitHub Actions:
astral-sh/setup-uv@v6withenable-cache: trueandcache-dependency-glob: "**/uv.lock". For matrix Python: passpython-versionto the action OR setUV_PYTHONenv var; do not also useactions/setup-pythonunless you setpython-preference: only-system. - Docker pattern: copy uv binary from
ghcr.io/astral-sh/uv:<tag>(pin a digest for supply-chain), thenuv sync --locked --no-install-project(deps layer), copy source,uv sync --locked(project layer). AlwaysUV_LINK_MODE=copyandUV_COMPILE_BYTECODE=1andUV_PYTHON_DOWNLOADS=neverin containers; add.venvto.dockerignore. - Publishing:
uv buildthenuv publish(supports PyPI trusted publishing via OIDC — no credentials in workflow).
Migration gotchas
From poetry:
- Caret/tilde version specs (
^1.2,~1.2) are NOT PEP 440.uvx migrate-to-uvtranslates^1.2→>=1.2,<2,~1.2→>=1.2,<1.3. Read the diff — the rewrite is occasionally wrong for pre-1.0 (poetry treats^0.x.yas>=0.x.y,<0.x+1). [tool.poetry.group.<name>.dependencies]→[dependency-groups](PEP 735). The migrator offers four strategies (set-default-groups,include-in-dev,merge-into-dev,keep-existing); pickset-default-groupsto retain semantics.poetry-dynamic-versioningplugin has no direct port — replace withuv-dynamic-versioning(separate project) OR switch build backend tohatchling+hatch-vcs. uv_build does not support dynamic versioning.poetry.lockanduv.lockare not interchangeable. Deletepoetry.lockafter migrating; do NOT try to import.tool.poetry.scripts→[project.scripts](standard PEP 621).
From pip-tools:
requirements.in→[project.dependencies]; devrequirements-dev.in→[dependency-groups] dev.uv add -r requirements.in -c requirements.txtpreserves pinned versions during import. Strip leading-r requirements.infrom the dev file before importing or you double-add.pip-compile foo.in -o foo.txtstill works asuv pip compile foo.in -o foo.txt— useful for projects not yet ready to fully port topyproject.toml.- Universal lock means a single
uv.lockreplaces per-platformrequirements-{linux,win,mac}.txt. If you need a per-platform export:uv export --format requirements.txt --python-platform <platform>.
From pipx:
pipx install ruff→uv tool install ruff.pipx run ruff→uvx ruff(oruv tool run ruff).- Tool envs live at
${UV_TOOL_DIR:-~/.local/share/uv/tools}. Not in your project venv.uv tool dirprints the path. uv tool install --with <extra>adds runtime extras to the tool env without exposing their executables.uv tool upgrade <tool>respects original constraints; reinstall to change them.uvxruns in a disposable cache env per invocation (cached, not regenerated, butuv cache cleanwipes it). For a stable installed CLI useuv tool install.
PEP 723 inline scripts
- A script becomes self-describing with a
# /// scriptblock:# /// script # requires-python = ">=3.12" # dependencies = ["httpx", "rich"] # /// uv add --script foo.py 'httpx>=0.25'injects/edits the block.uv lock --script foo.pywritesfoo.py.lockadjacent (per-script lockfile, notuv.lock).uv run foo.pyruns in an ephemeral env built from the script's metadata — does NOT touch the project venv. To force project mode, drop the metadata block; to force script mode inside a project useuv run --script foo.py(or shebang#!/usr/bin/env -S uv run --script).--with <pkg>adds a one-off dep without rewriting the metadata block — useful for ad-hoc REPL/debug.
Authoritative references
Official uv docs (docs.astral.sh/uv):
- Project layout &
pyproject.toml - Locking and syncing
- Managing dependencies &
[tool.uv.sources] - Workspaces
- Resolution (universal, markers, strategies)
- Python versions & toolchain
- Tools (
uv tool,uvx) - Cache management
- Build backend (
uv_build) - Configuration /
[tool.uv.environments] - Environment variables reference
- Scripts (PEP 723)
- Docker integration
- GitHub Actions integration
- PyTorch integration (alternative indexes)
- pip-to-uv migration
Repo / changelog: astral-sh/uv on GitHub — read the CHANGELOG before pinning a version; 0.10 is the current major behavioural cut.
Migration tooling: migrate-to-uv (run as uvx migrate-to-uv) — supports poetry, pipenv, pip-tools, pip.
Community deep-dives:
- pydevtools handbook —
uvvspoetry,uvvspip, dependency-group semantics - SaaS Pegasus uv deep-dive
- Charlie Marsh on uv build backend stability
Guardrails
Before recommending a non-trivial uv configuration change (build-isolation off, unsafe-best-match, tool.uv.environments narrowing, --frozen in CI):
- Quote the exact field/flag and its default.
- Cite the relevant uv docs section.
- State the failure mode the change accepts (e.g., dependency-confusion risk for
unsafe-best-match; stale lock for--frozen). - Verify the user's uv version supports the feature — many flags (e.g.,
extra-build-dependencies,--no-default-groups, named-required indexes) landed in 0.10. Runuv --versionfirst.
Universal resolution and build isolation are correctness defaults — disable them only with a stated reason.