uv combines environment creation, Python version management, dependency resolution, lockfiles, and command execution. It does not change how Python code is written; it makes project setup and reproduction more explicit.

If these files are new to you, review pyproject.toml and virtual environments first.

Create and run a project

Install uv using its official installation guide, then run:

uv init analyzer
cd analyzer
uv add requests
uv run python main.py

uv add records the dependency in pyproject.toml, resolves versions, and updates uv.lock. The .venv environment is created when needed. Do not commit .venv; commit project configuration and, for an application, the lockfile.

After cloning the repository elsewhere:

uv sync
uv run pytest

uv sync makes the environment match the lockfile. Add development tools separately:

uv add --dev pytest ruff

This also works well in GitHub Actions because local and CI commands remain identical.

Migrate from requirements.txt

Test the migration on a branch instead of deleting the old setup immediately:

uv init
uv add -r requirements.txt
uv lock
uv sync

Run the full test suite and application. Unbounded requirements may resolve differently, so compare behavior rather than package names alone. The official project guide explains environments, locking, execution, and builds.

Practical rules

  • Declare supported Python versions with requires-python.
  • Use uv add and uv remove instead of modifying the environment manually.
  • Review uv.lock changes like any dependency update.
  • Run tests after upgrades.
  • Avoid mixing project managers without a documented reason.

uv's main benefit is not speed alone. It provides one visible path from project metadata to local execution and CI. Adopt it in a small project first, verify the lockfile in automation, and then consider critical applications.

Know what each project file does

Configuration, resolution, and installation are related but distinct. pyproject.toml declares the project's intent: its name, supported Python range, and direct dependencies. uv.lock records a complete resolution, including transitive packages and artifacts for supported platforms. .venv is the disposable local installation built from those inputs.

That distinction is why deleting .venv does not delete meaningful project state. If the tracked files are valid, uv sync can rebuild the environment. It also explains why installing a package through a pip-compatible command is not equivalent to uv add: an installation can alter the environment without recording the dependency as a project decision.

Applications generally benefit from committing uv.lock because deployment and tests consume a reviewed solution. A published library may also use the lockfile for development, but its users resolve dependencies from the ranges in package metadata. Library maintainers should therefore test meaningful compatibility boundaries instead of relying only on the locked development combination.

Separate runtime and development dependencies

Keep packages required by the running application separate from test, lint, and documentation tools:

uv add fastapi
uv add --dev pytest ruff
uv remove fastapi

After an add, remove, or upgrade, inspect both pyproject.toml and the uv.lock diff. One direct change can legitimately move several transitive packages. That is not automatically unsafe, but it deserves the same review as a code change.

Dependency groups can model optional workflows such as documentation or integration tests. Avoid creating a group for every command. Document which groups CI installs and verify the syntax against the uv version used by the project. A small, consistent convention is easier to reproduce than a flexible setup nobody fully understands.

Select Python deliberately

uv can download and select Python interpreters, while requires-python remains the project's compatibility contract:

[project]
requires-python = ">=3.12"

The resolver uses this range, so it is more than a note for readers. To give contributors a default interpreter, pin one for the working tree:

uv python install 3.12
uv python pin 3.12

Do not widen the supported range without tests. Code written on 3.12 may use syntax unavailable on 3.10, and dependencies may support a different range. Libraries still need a test matrix when they claim several Python versions. An environment manager selects interpreters; it cannot prove behavior across them.

Run commands in a predictable context

uv run makes the project's environment available and ensures it is synchronized when necessary. It removes hidden activation steps from documentation and automation:

uv run python -m pytest
uv run ruff check .
uv run python -m analyzer

Using python -m pytest makes the selected interpreter explicit. Activating .venv can remain convenient in an interactive shell, but builds should not depend on a previous activation command.

For a standalone tool that does not belong to the project, uvx can run it in an isolated environment. Use that convenience carefully. If the exact Ruff or code-generator version changes committed output, pin or declare the tool so every contributor and CI job produces compatible results.

Upgrade without turning sync into a surprise

Synchronization and upgrading are different operations. uv sync should reproduce the current lockfile; an intentional upgrade should create a reviewable lockfile change and be followed by tests.

uv lock --upgrade-package requests
uv sync
uv run pytest

Prefer focused upgrades. Read release notes for important libraries and investigate breaking changes before crossing a major version. Resolve dependencies during a reviewed development change, not for the first time during production deployment. Production should consume the approved decision.

When resolution fails, inspect the Python range, direct constraints, and transitive constraints named in the explanation. Removing every upper bound may silence the immediate conflict while creating an unsupported combination. Identify which package makes the constraints incompatible, then update, replace, or temporarily hold it with a documented reason.

Make CI and deployment reproducible

The official uv guide for GitHub Actions documents the maintained setup action and current options. A typical pipeline checks out the repository, installs the intended Python and uv versions, synchronizes dependencies, then runs lint and tests.

Treat caches as performance improvements only. A clean runner with an empty cache must still succeed. Never commit .venv or copy an environment between operating systems. The lockfile can describe platform-specific artifacts, but an installed environment is not portable.

For container builds, copying dependency metadata before application code can preserve a useful layer cache. Verify that the local package is installed when the application imports it and that the final command uses the intended environment. A successful dependency layer is not proof that source packaging is correct.

Common adoption mistakes

  • Keeping requirements.txt, Poetry commands, and uv active indefinitely without choosing a source of truth.
  • Editing uv.lock manually instead of generating it with the resolver.
  • Ignoring a large lockfile diff because only one direct package was requested.
  • Treating uv sync as a request to upgrade everything.
  • Pinning one interpreter locally while declaring an incompatible requires-python range.
  • Running an unpinned uvx generator whose output is committed.
  • Performing the first dependency resolution on the production host.

During migration, keep the old workflow only for a short comparison period. Once tests, developer commands, and deployment work with uv, remove obsolete instructions or schedule that removal explicitly. Two aging sources of truth are harder to diagnose than a deliberate cutover.

Adoption checklist

Before calling the migration complete, clone the repository into a clean directory. Confirm that uv sync rebuilds the environment, .venv is ignored, direct dependencies are justified in pyproject.toml, and the reviewed lockfile is committed. Verify that the Python range matches reality and that local documentation and CI execute the same commands.

Then run the application and full test suite without relying on global packages. This clean-room check finds missing files, accidental global installations, and commands that only work in the original checkout. That verifiable path from metadata to a working process, rather than speed by itself, is the strongest reason to adopt uv.