Skip to content

How to lock uv script dependencies for reproducible execution

uv

A self-contained script with PEP 723 inline metadata declares loose dependency ranges like requests<3. Two machines running it a week apart can resolve different versions. To make a script reproducible, lock those ranges to exact versions with a lockfile, the same way that a project pins its dependencies in uv.lock.

uv locks a single-file script without a surrounding project. This guide shows how to generate a script-adjacent lockfile and enforce it in shared automation.

Prerequisites

  • uv installed, version 0.11.4 or newer (run uv self update to upgrade). Earlier versions accept --locked for scripts but do not enforce it.
  • A script with PEP 723 inline metadata. If you don’t have one, scaffold it with uv init --script report.py --python 3.12 and add dependencies with uv add --script report.py requests rich.

Lock the script’s dependencies

Run uv lock with the --script flag pointed at your script:

uv lock --script report.py

uv resolves the inline metadata and writes a lockfile named after the script:

$ uv lock --script report.py
Resolved 9 packages in 147ms
$ ls report.py.lock
report.py.lock

The lockfile is report.py.lock, sitting next to report.py. It pins every direct and transitive dependency to an exact version with hashes. That makes it the script-level equivalent of a project’s root-level uv.lock. Commit report.py.lock alongside the script so everyone who runs it resolves the same versions.

Run the script from the lockfile

Once the lockfile exists, uv run --script uses it automatically. No extra flag is needed for the everyday case:

$ uv run --script report.py
Installed 9 packages in 36ms
Hello from report.py!

uv reads report.py.lock and runs the script from the pinned versions.

Enforce the lockfile in CI with --locked

In automation, you want the run to fail when the lockfile no longer matches the script rather than silently re-resolving. Pass --locked:

uv run --locked --script report.py

If the inline metadata has changed since the last lock (someone added a dependency without re-locking), uv refuses to proceed:

$ uv run --locked --script report.py
error: The lockfile at `uv.lock` needs to be updated, but `--locked` was provided. To update the lockfile, run `uv lock`.

The command exits non-zero, so the pipeline stops. Add --locked to the shebang to make every direct invocation strict:

#!/usr/bin/env -S uv run --locked --script

Tip

Use --frozen instead of --locked to install strictly from the existing lockfile without checking it against the inline metadata. This skips resolution entirely, which is useful on an offline runner or when you want the fastest possible cold start.

Update the lockfile when dependencies change

Adding a dependency refreshes the lockfile in the same step, once the lockfile exists:

uv add --script report.py httpx

To refresh pinned versions for a security update without changing the declared ranges, upgrade and re-lock:

uv lock --upgrade --script report.py

Bump a single package while holding the rest in place:

uv lock --upgrade-package requests --script report.py

Commit the updated report.py.lock with the change.

Export the locked set for non-uv consumers

When a downstream tool expects a requirements.txt, export the pinned dependencies with hashes from the script lockfile:

uv export --script report.py -o requirements.txt

The output is a fully pinned, hash-checked requirements file that pip install -r requirements.txt --require-hashes can consume on a machine without uv.

Learn more

Last updated on