A Python application can work correctly on one developer's machine and fail on another because the installed package versions are different. A dependency update may introduce a breaking change, a transitive dependency may resolve to a different version, or a fresh CI runner may install packages that were never tested together.

The uv tool helps manage Python projects, dependencies, virtual environments, and lock files. By committing a uv.lock file and using it consistently across development and CI, teams can make dependency installation more predictable.

This article explains how to create a Python project with uv, generate and maintain a lock file, install locked dependencies in GitHub Actions, and troubleshoot common dependency problems.

Why Python Projects Need a Lock File

Python projects commonly declare dependencies in a pyproject.toml file.

For example:

[project]
name = "customer-service"
version = "0.1.0"
requires-python = ">=3.11"
dependencies = [
    "fastapi>=0.115",
    "httpx>=0.27",
    "pydantic>=2.9",
]

These version constraints define which versions are acceptable. They do not necessarily identify one exact set of package versions.

Suppose a project allows any compatible release of a library newer than version 0.115. A fresh installation may resolve a newer release than the one installed last week. That release may introduce a behavior change or require a different version of another package.

The same issue applies to transitive dependencies: packages installed because another package requires them.

A lock file records the resolved dependency set, including the versions of direct and transitive packages and the information needed to obtain them. In a uv-managed project, that file is named uv.lock.

The distinction is important:

  • pyproject.toml describes the project's declared dependencies and constraints.

  • uv.lock records a resolved set of dependencies.

  • The project environment contains the packages actually installed for local development or execution.

These files serve different purposes and should not be treated as interchangeable.

Install uv and Create a Project

Install uv using the official installation method appropriate for your operating system. In a CI environment, pin the uv version rather than relying on an unbounded latest version.

After installation, verify that the command is available:

uv --version

Create a new project:

uv init customer-service
cd customer-service

For an existing Python application, you can also initialize uv management in the current repository, provided you review the generated configuration and preserve the existing project structure.

Add the dependencies required by the application:

uv add fastapi httpx pydantic

The command updates pyproject.toml, resolves the dependencies, and creates or updates uv.lock.

For development tools such as pytest, add development dependencies separately:

uv add --dev pytest ruff

Keeping development dependencies separate makes it easier to distinguish packages required to run the application from packages needed for testing and code quality checks.

Generate and Commit uv.lock

When you add or change dependencies, uv resolves a compatible dependency set and updates the lock file.

You can explicitly refresh the lock file with:

uv lock

Inspect the resulting changes before committing them:

git status
git diff -- pyproject.toml uv.lock

Commit both files:

git add pyproject.toml uv.lock
git commit -m "Add locked Python dependencies"

The lock file should normally be committed to source control for applications, services, and other projects where repeatable installation matters.

Do not edit uv.lock manually. Use uv commands to update the dependency graph so the lock file remains consistent with the project configuration.

Install the Exact Locked Dependencies

After committing the lock file, developers and CI pipelines should install dependencies from that file rather than resolving a new dependency set on every run.

For a project environment, use:

uv sync --locked

The --locked option tells uv to fail if the lock file needs to be updated to match the project configuration. It does not silently regenerate the lock file.

This is useful in CI because a dependency configuration change that was not accompanied by a lock-file update becomes a visible failure instead of an unnoticed dependency change.

For CI environments where the project should not include development dependencies, use:

uv sync --locked --no-dev

This installs the project's production dependencies without its development dependency group.

The exact set of groups installed should match the purpose of the environment. A test job usually needs development dependencies, while a production image may need only runtime dependencies.

Update Dependencies Without Losing Reproducibility

Lock files do not mean dependencies should never change. They make changes deliberate and reviewable.

When you want uv to resolve newer versions allowed by the project's constraints, run:

uv lock --upgrade

Review the changes to uv.lock, run the test suite, and commit the updated file.

If only one package needs an update, use a targeted upgrade:

uv lock --upgrade-package httpx

This asks uv to upgrade the specified package while resolving a compatible dependency graph.

The exact result depends on the version constraints in pyproject.toml. If a package is pinned to a narrow version range, uv cannot select a release outside that range without a corresponding configuration change.

Avoid running unrestricted upgrades automatically in every production build. Updating dependencies should be a controlled change that can be tested and reviewed.

Use uv in GitHub Actions

A CI pipeline should use a consistent Python version, a pinned uv version, and the committed lock file.

The following workflow installs dependencies, runs tests, and checks code quality:

name: Python CI

on:
  push:
    branches:
      - main
  pull_request:

jobs:
  test:
    runs-on: ubuntu-latest

    steps:
      - name: Check out repository
        uses: actions/checkout@v4

      - name: Set up Python
        uses: actions/setup-python@v5
        with:
          python-version: "3.12"

      - name: Install uv
        uses: astral-sh/setup-uv@v6
        with:
          version: "0.9.0"

      - name: Install dependencies
        run: uv sync --locked

      - name: Run tests
        run: uv run --locked pytest

      - name: Check formatting and lint
        run: uv run --locked ruff check .

The uv version shown is an example pin. Select a version that has been tested with your project and update it deliberately when needed. Likewise, choose a Python version supported by your application and dependencies.

The uv run --locked command checks the lock-file state before running the requested command. This helps catch configuration drift during CI.

The workflow assumes the repository contains a valid uv-managed project and that the required development tools are declared in pyproject.toml.

For a project with a different layout, update the test command and tool configuration to match the repository.

Cache uv Downloads in GitHub Actions

Locking dependencies improves repeatability, but it does not eliminate the time spent downloading packages on fresh CI runners.

uv uses a cache for downloaded and built package artifacts. Caching this directory can reduce repeated network downloads and package-building work.

The following workflow adds caching:

name: Python CI

on:
  push:
    branches:
      - main
  pull_request:

jobs:
  test:
    runs-on: ubuntu-latest

    steps:
      - name: Check out repository
        uses: actions/checkout@v4

      - name: Set up Python
        uses: actions/setup-python@v5
        with:
          python-version: "3.12"

      - name: Install uv
        uses: astral-sh/setup-uv@v6
        with:
          version: "0.9.0"
          enable-cache: true
          cache-dependency-glob: |
            uv.lock
            pyproject.toml

      - name: Install dependencies
        run: uv sync --locked

      - name: Run tests
        run: uv run --locked pytest

The setup action manages the cache for uv. The dependency glob tells it which files should influence cache invalidation.

If your repository has multiple projects or lock files, adjust the glob patterns to include the files used by the workflow. A cache key should reflect dependency changes without being invalidated by every source-code edit.

Caching is a performance optimization, not a replacement for dependency locking. The workflow must still install and validate the dependencies required by the current lock file.

Understand What a Lock File Guarantees

A committed lock file improves consistency, but it does not make every aspect of a build identical across all machines.

The installed environment can still depend on factors such as:

  • Python version and platform.

  • Operating system and CPU architecture.

  • Availability of compatible package wheels.

  • Environment variables and external services.

  • Native system libraries required by certain packages.

  • Build tools used to compile packages from source.

For example, a package may provide a prebuilt wheel for Linux but require compilation on another platform. The resolved Python package versions may be the same, while the build process differs.

For stronger reproducibility, pin the Python version, use consistent build environments, and document required system dependencies. For applications that require tightly controlled deployment artifacts, build and test a container image with a defined base image and architecture.

Common Problems and How to Fix Them

CI reports that the lock file is out of date

This usually means pyproject.toml changed without a corresponding lock-file update.

Run:

uv lock

Review the resulting changes and commit the updated uv.lock file. Do not remove --locked from CI just to make the failure disappear, because doing so can hide an incomplete dependency update.

A package version cannot be resolved

Check the constraints in pyproject.toml, the Python version requirement, and any package sources configured for the project.

A dependency may require a newer Python version or conflict with another package's version constraints. Resolve the conflict intentionally rather than repeatedly upgrading all packages without examining the dependency graph.

The CI cache does not improve build time

Check whether the workflow is restoring the cache and whether the dependency files are included in its cache configuration.

Also determine where time is actually spent. If most of the workflow duration comes from compiling application code or running tests, package caching may have only a small effect on the total build time.

Local and CI environments behave differently

Confirm that both environments use the committed lock file and compatible Python versions. Also inspect environment variables, operating-system dependencies, and platform-specific package behavior.

A lock file cannot compensate for different configuration values, missing native libraries, or tests that depend on external services.

Recommended Workflow for Teams

For an application managed with uv, use a straightforward dependency process:

  1. Declare direct dependencies in pyproject.toml.

  2. Generate and review uv.lock when dependencies change.

  3. Commit both files to source control.

  4. Use uv sync --locked in CI.

  5. Run tests and code-quality checks against the installed environment.

  6. Cache uv downloads to reduce repeated installation work.

  7. Update dependencies through reviewed changes rather than automatically changing them in every build.

This process gives developers a consistent dependency baseline while still allowing regular security updates and maintenance.

Conclusion

uv provides a practical way to manage Python dependencies and make builds more repeatable. The key is to commit uv.lock, install dependencies with uv sync --locked, and keep the Python version and build environment consistent.

Adding cache support in GitHub Actions can further reduce dependency installation time. Together, dependency locking and caching help teams build Python applications with fewer unexpected package changes and more predictable CI results.