# How to use a uv lockfile for reproducible Python environments

Commit `uv.lock` to version control. It records the exact version and hash of every package your project depends on, including transitive dependencies, so [uv](https://pydevtools.com/handbook/reference/uv.md) installs the identical tree on every machine and every deployment. This guide shows how to create, commit, refresh, and troubleshoot the [lockfile](https://pydevtools.com/handbook/explanation/what-is-a-lock-file.md), and how to wire it into CI and Docker without silent drift.

## Create your first lockfile

Start a new project and add dependencies:

```bash
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]
> Never edit `uv.lock` by hand. uv regenerates it whenever `pyproject.toml` changes, so manual edits are overwritten.

## Sync your environment from the lockfile

`uv sync` makes your virtual environment match the lockfile:

```bash
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:

```bash
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:

```bash
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:

```bash
uv lock --upgrade
```

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

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

Upgrade every package in one [dependency group](https://pydevtools.com/handbook/explanation/understanding-dependency-groups-in-uv.md):

```bash
uv lock --upgrade-group dev
```

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

```bash
uv lock --upgrade-package requests --dry-run
```

```console
$ 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:

```bash
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:

```dockerfile
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](https://pydevtools.com/handbook/how-to/how-to-use-uv-in-a-dockerfile.md).

## Fix common lockfile errors

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

```console
$ 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:

```console
$ 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-newer` for reproducible Python environments](https://pydevtools.com/handbook/how-to/how-to-use-exclude-newer-for-reproducible-python-environments.md) time-locks resolution when you have no lockfile to start from.
- [What is a lockfile?](https://pydevtools.com/handbook/explanation/what-is-a-lock-file.md) explains why pinned, hashed dependencies matter.
- [uv: A Complete Guide](https://pydevtools.com/handbook/explanation/uv-complete-guide.md) covers what uv does, how fast it is, and the core workflows.
- [uv documentation on locking and syncing](https://docs.astral.sh/uv/concepts/projects/sync/)
