Skip to content

How to use a uv lockfile for reproducible Python environments

uv

A lockfile pins every dependency to an exact version so your Python project installs identically on every machine and every deployment. This guide shows how to create, commit, refresh, and troubleshoot uv’s uv.lock, and how to wire it into CI and Docker without silent drift.

Create your first lockfile

Start a new project and add dependencies:

uv init reproducible-demo
cd reproducible-demo
uv add requests pandas

uv writes uv.lock with pinned versions of requests, pandas, and every transitive dependency. uv.lock is a universal lockfile: one file resolves across operating systems, architectures, and Python versions. A teammate on Windows and a Linux CI runner install identical versions from it.

Important

Commit uv.lock to version control. It is the contract that makes every later install reproducible. Never edit it by hand; uv regenerates it.

Sync your environment from the lockfile

uv sync makes your virtual environment match the lockfile:

uv sync

The behavior depends on the lockfile state:

  • If uv.lock matches pyproject.toml, uv installs the exact versions from the lockfile.
  • If the lockfile is stale or missing, uv re-resolves, rewrites uv.lock, then installs.

That automatic re-locking is convenient in development but dangerous in CI, where a silent update defeats the point of a lockfile. Two flags turn it off:

uv sync --locked   # error if uv.lock is out of date with pyproject.toml
uv sync --frozen   # install from uv.lock as-is, never checking pyproject.toml

Use --locked when you want the build to fail on drift. Use --frozen when you have already validated the lockfile and want the fastest install with no resolution step.

When should you re-lock?

Re-lock whenever the inputs to resolution change:

  • After editing dependencies in pyproject.toml by hand (uv add and uv remove re-lock for you).
  • After pulling changes that touched pyproject.toml or uv.lock.
  • To pick up security or feature releases with uv lock --upgrade, even when pyproject.toml is unchanged.

To refresh the lockfile without touching the environment, run uv lock on its own. To verify a lockfile is current without changing anything, run:

uv lock --check

This exits non-zero if uv.lock no longer matches pyproject.toml, which makes it a fast pre-deploy gate.

Upgrade dependencies safely

uv lock keeps existing versions pinned unless you explicitly allow upgrades. Update everything to the latest compatible versions:

uv lock --upgrade

Upgrade a single package, or pin it to a target version, while leaving the rest of the tree untouched:

uv lock --upgrade-package requests
uv lock --upgrade-package 'pandas==2.2.3'

Upgrade every package in one dependency group:

uv lock --upgrade-group dev

Preview any upgrade before writing the lockfile with --dry-run:

uv lock --upgrade-package requests --dry-run
$ uv lock --upgrade-package requests --dry-run
Resolved 6 packages in 83ms
Update requests v2.32.3 -> v2.34.2

All three upgrade flags also work on uv sync to update and install in one step, for example uv sync --upgrade-package requests.

Lock the same versions in CI and Docker

In CI, install strictly from the committed lockfile and fail if it has drifted:

uv sync --locked

If --locked fails, someone changed pyproject.toml without re-locking. Run uv lock locally and commit the updated uv.lock.

In Docker, copy the lockfile and pyproject.toml before the source code so the dependency layer caches independently of application changes:

COPY pyproject.toml uv.lock ./
RUN uv sync --locked --no-install-project
COPY . .
RUN uv sync --locked

--no-install-project installs dependencies but not your own package, so editing source code invalidates only the final cheap layer, not the expensive dependency layer above it. For a full container walkthrough, see How to use uv in a Dockerfile.

Fix common lockfile errors

“The lockfile needs to be updated, but --locked was provided.” The committed uv.lock no longer matches pyproject.toml:

$ uv sync --locked
The lockfile at `uv.lock` needs to be updated, but `--locked` was provided.
To update the lockfile, run `uv lock`.

Run uv lock, commit the result, and the CI step passes. The same message appears for uv lock --check.

“No solution found when resolving dependencies.” Two requirements cannot be satisfied at once:

$ uv add 'urllib3>=2' 'botocore==1.29.0'
  × No solution found when resolving dependencies:
  ╰─▶ Because botocore==1.29.0 depends on urllib3>=1.25.4,<1.27 and your
      project depends on botocore==1.29.0, we can conclude that your project
      depends on urllib3>=1.25.4,<1.27.
      And because your project depends on urllib3>=2, we can conclude that
      your project's requirements are unsatisfiable.

uv names the exact packages and the conflicting ranges. Loosen one constraint (here, drop the urllib3>=2 pin and let botocore choose), or upgrade the package whose pin is forcing the old range.

A teammate’s lockfile won’t reproduce. If uv sync re-resolves to different versions on another machine, the lockfile was probably never committed, or someone ran a plain uv sync that silently updated it. Confirm uv.lock is tracked in git and switch CI to uv sync --locked so drift fails loudly instead of slipping through.

Should you commit uv.lock?

Yes, for any application or service. The committed lockfile is what guarantees that your laptop, your teammates’ machines, CI, and production all install the identical dependency tree.

The one exception is libraries published to PyPI. Downstream installers resolve their own versions from your pyproject.toml ranges, so your uv.lock governs only your own development environment. Commit it anyway for reproducible local work, but know it has no effect on people who install your package.

Learn more

Last updated on