Manage Python Dependencies and Environments with uv

Python projects often accumulate a patchwork of commands: one tool installs Python, another creates a virtual environment, pip installs packages, a requirements file pins versions, and separate utilities run command-line tools. That works, but every extra moving part creates another place where a teammate or CI runner can reproduce a different environment.

uv combines Python installation, project metadata, dependency resolution, lockfiles, virtual environments, and command execution in one tool. It follows standard pyproject.toml metadata while maintaining a cross-platform uv.lock file for exact resolutions.

As verified on September 6, 2026, uv 0.12.10 is the latest release, published September 4, 2026. This tutorial uses stable project commands documented for that release and shows how to keep local development and continuous integration aligned.

Install uv and verify what you are running

On macOS or Linux, the official standalone installer is:

curl -LsSf https://astral.sh/uv/install.sh | sh

On Windows PowerShell:

powershell -ExecutionPolicy ByPass -c "irm https://astral.sh/uv/install.ps1 | iex"

Restart the shell if the installer changed your PATH, then verify the binary:

uv --version
uv help

For controlled environments, request a specific installer version instead of silently taking the newest release:

curl -LsSf https://astral.sh/uv/0.12.10/install.sh | sh

You can also install uv with Homebrew, WinGet, Scoop, PyPI, Cargo, or release binaries. Upgrade it through the same mechanism used to install it. uv self update is intended for standalone-installer installations; package-manager installations should use their package manager.

Downloading a script and piping it into a shell is convenient but requires trust. In restricted environments, inspect the script first or download a signed/released binary according to your organization's software-supply policy.

Create a project with a pinned Python version

Create an application project:

uv init task-api
cd task-api
uv python pin 3.14

The project starts with standard metadata and source files. The pin writes a .python-version file, which tells uv which interpreter to use for the project environment. If the interpreter is unavailable, install it through uv:

uv python install 3.14

The important files are:

task-api/
├── .python-version
├── pyproject.toml
├── README.md
└── src/

After the first project operation, uv also creates .venv/ and uv.lock. Commit .python-version, pyproject.toml, and uv.lock. Do not commit .venv; it is machine-specific and can be reproduced.

Your pyproject.toml contains the supported Python range and dependency declarations:

[project]
name = "task-api"
version = "0.1.0"
requires-python = ">=3.14"
dependencies = []

The version pin selects a local interpreter. requires-python declares compatibility to resolvers and package consumers. Keep them consistent, but understand that they solve different problems.

Add runtime and development dependencies

Add packages through uv instead of editing several files manually:

uv add fastapi uvicorn httpx
uv add --group dev pytest ruff

uv add updates pyproject.toml, resolves the dependency graph, refreshes uv.lock, and synchronizes the project environment. Runtime packages appear under [project].dependencies; development tools appear in a dependency group.

Add an explicit compatibility constraint when your application depends on a supported range:

uv add 'httpx>=0.28,<0.29'

Avoid pinning every direct dependency to one exact version in pyproject.toml without a reason. Broad, tested constraints express what the project supports; the lockfile records the exact environment currently selected.

Remove packages with the same discipline:

uv remove httpx
uv remove --group dev ruff

Do not use pip install inside the managed environment as a routine workflow. An undeclared package may work locally but disappear when another developer runs an exact sync.

Understand the lockfile and environment

uv.lock is a human-readable, cross-platform lockfile managed by uv. It records exact resolved versions and source information. Commit it for applications so developers and CI resolve from the same plan.

Create or update it explicitly with:

uv lock

Check whether project metadata and the lockfile agree without changing anything:

uv lock --check

A newly released package does not automatically make the lockfile outdated. uv updates locked versions only when you explicitly request an upgrade or change constraints. That protects routine installs from unreviewed version drift.

The project environment lives in .venv by default. You usually do not need to activate it because uv run selects it automatically:

uv run python --version
uv run python -m task_api
uv run pytest
uv run ruff check .

This is useful in scripts, editor tasks, and documentation because the command does not depend on shell activation state.

Sync reproducibly on a new machine

After cloning the repository:

git clone https://example.com/team/task-api.git
cd task-api
uv python install
uv sync --locked

uv sync installs packages from the lockfile into the project environment. It performs an exact sync by default, removing packages that are not represented in the lockfile. This catches accidental local dependencies and keeps the environment predictable.

--locked checks that the lockfile matches project metadata. If a developer changed pyproject.toml but forgot to update uv.lock, the command fails instead of silently rewriting the lock during deployment.

There are three related modes worth distinguishing:

  • --locked verifies that the lock is current and fails if it needs an update;
  • --frozen uses the lockfile without checking whether it matches project metadata;
  • --no-sync tells uv run not to verify or update the environment before running.

Use --locked for normal CI and deployment. --frozen and --no-sync are specialized optimizations; they can hide inconsistency if earlier pipeline steps did not establish the required state.

Development groups are included by default. For a production image that needs only application dependencies:

uv sync --locked --no-dev

If the project defines optional dependencies, select them explicitly:

uv sync --locked --extra postgres

Do not use --all-extras automatically in production. Extras may represent mutually exclusive backends or large optional toolchains.

Upgrade dependencies deliberately

Inspect the current dependency tree:

uv tree

Upgrade one package within declared constraints:

uv lock --upgrade-package httpx
uv sync

Upgrade the whole resolution only when you are prepared to review and test the resulting changes:

uv lock --upgrade
uv run pytest
uv run ruff check .
git diff -- pyproject.toml uv.lock

Treat lockfile changes like source-code changes. Review which direct and transitive versions moved, run the test suite, and keep the update in its own pull request when practical. Reproducibility does not guarantee compatibility; it guarantees that everyone can reproduce the same selected versions.

Build a small application workflow

Create a minimal FastAPI application:

# src/task_api/main.py
from fastapi import FastAPI

app = FastAPI()


@app.get("/health")
def health() -> dict[str, str]:
    return {"status": "ok"}

Run it through the managed environment:

uv run uvicorn task_api.main:app --reload

Add a test:

# tests/test_health.py
from fastapi.testclient import TestClient
from task_api.main import app

client = TestClient(app)


def test_health() -> None:
    response = client.get("/health")
    assert response.status_code == 200
    assert response.json() == {"status": "ok"}

Execute all project checks with explicit commands:

uv run pytest
uv run ruff check .

These commands automatically ensure the lock and environment are current during development. In CI, synchronize with --locked first so an accidental metadata change produces an obvious failure.

Migrate an existing requirements-based project

Create or inspect the project's pyproject.toml, then import direct requirements:

uv init --bare
uv add -r requirements.in
uv add --group dev -r requirements-dev.txt
uv lock
uv sync

Migration deserves review. A legacy requirements.txt may contain both direct and transitive pins, platform-specific packages, editable installs, or private indexes. Decide which packages are intentional direct dependencies before deleting the old files.

Run the old and new test suites from clean environments and compare behavior. Keep a rollback branch until deployment succeeds with uv sync --locked.

Troubleshooting common problems

uv is installed but the command is not found

Restart the shell and inspect PATH. Standalone binaries commonly live under the user's local binary directory. Avoid adding a world-writable directory to PATH merely to make the command visible.

The lockfile is reported as outdated

Someone changed dependency metadata without updating uv.lock. Run uv lock, review the diff, test it, and commit both files. Do not replace --locked with --frozen just to make CI green.

A module imports locally but fails in CI

The package may have been installed manually and never declared. Recreate the environment with uv sync, then add the missing direct dependency with uv add or the correct dependency group.

uv sync removed a package

Exact syncing removes extraneous packages by design. Declare the package or, for a deliberate local experiment, use an isolated tool or temporary environment. --inexact retains extra packages but weakens reproducibility.

The wrong Python version is selected

Compare .python-version, requires-python, and uv run python --version. Install the intended interpreter with uv python install and update the pin deliberately.

A package cannot build on one platform

Check whether it publishes a compatible wheel and whether system libraries or compilers are required. The lockfile can represent multiple platforms, but it cannot make a package portable when the package itself lacks support.

CI is unexpectedly changing uv.lock

Use uv sync --locked or uv lock --check before tests. CI should validate committed dependency decisions, not create new ones during an ordinary test run.

Production checklist

  • Install uv through a documented, trusted method.
  • Pin the uv release in CI and deployment workflows.
  • Declare the supported Python range in pyproject.toml.
  • Commit .python-version, pyproject.toml, and uv.lock.
  • Exclude .venv from version control.
  • Add and remove packages through uv commands.
  • Separate runtime and development dependency groups.
  • Use uv sync --locked in CI and deployment.
  • Use --no-dev for production environments that do not need tooling.
  • Review and test every lockfile upgrade.
  • Treat caches as disposable optimizations.
  • Rebuild from a clean environment before release.
  • Document private indexes and authentication without committing secrets.

Official references