How to use a uv lockfile for reproducible Python environments
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 pandasuv 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 syncThe behavior depends on the lockfile state:
- If
uv.lockmatchespyproject.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.tomlUse --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.tomlby hand (uv addanduv removere-lock for you). - After pulling changes that touched
pyproject.tomloruv.lock. - To pick up security or feature releases with
uv lock --upgrade, even whenpyproject.tomlis 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 --checkThis 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 --upgradeUpgrade 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 devPreview 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 --lockedIf --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
- How to use
--exclude-newerfor reproducible Python environments time-locks resolution when you have no lockfile to start from. - What is a lockfile? explains why pinned, hashed dependencies matter.
- uv: A Complete Guide covers what uv does, how fast it is, and the core workflows.
- uv documentation on locking and syncing