Overview

Use uv for Python versions, virtualenvs, dependency resolution, lockfiles, builds, and publishing. pyproject.toml is the single source of truth for metadata, dependencies, and tool settings, which is the same one-source rule as config-files.

Add dependencies with uv, never with pip

pip install inside a uv project bypasses the lockfile.

uv init                  # new project (add --package for a publishable library)
uv add httpx             # runtime dependency
uv add --dev pytest      # writes to [dependency-groups] dev
uv run pytest            # run inside the project env
uv lock                  # re-resolve uv.lock

uv sync installs exactly what the lockfile lists and removes anything else; uv run leaves extra packages in place. The legacy [tool.uv] dev-dependencies field is on a path to deprecation, so use [dependency-groups].

Separate groups from extras

A [dependency-groups] entry is local to the repo and never published. The dev group is synced by default; exclude it with --no-dev. [project.optional-dependencies] extras are public features that users install as pkg[extra].

[dependency-groups]
dev = ["ruff", "pyright"]
test = ["pytest", "pytest-asyncio", "pytest-cov", "hypothesis"]

Install uv sync --locked --no-dev in production images, and uv sync --locked --group test where only tests run.

Commit uv.lock and fail CI when it is stale

Commit uv.lock for applications and libraries; it pins every transitive version and is never part of the published wheel. In CI use --locked, which errors when the lockfile does not match pyproject.toml. --frozen is different: it skips the up-to-date check and trusts the file as is.

uv sync --locked

Pin the interpreter and the support floor separately

requires-python is the oldest version you support and test; set it to what CI actually covers. uv python pin 3.14 writes .python-version, which uv uses as the default interpreter and downloads automatically if missing. Free-threaded builds are never selected implicitly; request 3.14t.

Install global tools with uv, not pipx

uv tool install ruff installs a CLI into an isolated environment, and uvx ruff check . runs one without installing. Use uv add --dev for tools tied to one project.

Audit dependencies in CI

pip-audit is the stable scanner. uv audit (OSV data, also flags deprecated packages) was announced in June 2026 and is in preview, so treat its output as advisory until it stabilizes. See python-security for triage rules.

uv export --format requirements-txt > /tmp/req.txt && pip-audit -r /tmp/req.txt

Build a publishable package

Put importable code under src/<name>/ so tests run against the installed package, not the working tree, and map commands in [project.scripts].

[project]
license = "MIT"          # SPDX expression (PEP 639); the { text = ... } table is deprecated
 
[project.scripts]
myapp = "mypackage.cli:main"
  • uv init --package defaults to the uv_build backend, which supports pure-Python code only. Keep the upper bound on its build-system.requires entry that uv writes, so a future uv release cannot break the build.
  • Use hatchling (with hatch-vcs for tag-derived versions) when you need build scripts or a flexible layout, and a backend built for compiled code when you ship extensions (build platform wheels in CI with cibuildwheel).
  • Run uv build --no-sources before release to confirm the package builds without uv-specific source overrides.

Publish with Trusted Publishing

uv publish uploads dist/*. On GitHub Actions with a PyPI trusted publisher configured, it needs no stored credentials. It uploads PEP 740 attestations found next to the files in dist/ but does not generate them. Fall back to a project-scoped UV_PUBLISH_TOKEN stored as a CI secret only when trusted publishing is unavailable. Bump the version with uv version --bump minor.