Inline Python dependencies with PEP 723

A standalone script can declare its own packages and run reproducibly with uv.

For a one-file utility, a requirements.txt, virtual environment, and project directory can be more ceremony than value. PEP 723 gives the script a small, standard metadata block instead.

# /// script
# dependencies = [
#   "requests<3",
#   "rich",
# ]
# ///

import requests
from rich.pretty import pprint

resp = requests.get("https://peps.python.org/api/peps.json")
data = resp.json()
pprint([(key, value["title"]) for key, value in data.items()][:10])

The block starts with # /// script and ends with # ///. Every line inside is still a Python comment; remove # and the remaining text is TOML. dependencies is a list of PEP 508 requirement strings. A script can also state its Python requirement:

# /// script
# requires-python = ">=3.11"
# dependencies = ["requests<3"]
# ///

That means python script.py can still parse the file, while a tool such as uv can read the script block, resolve an isolated environment, and execute it.

Run and update it

uv run script.py

No activation step and no checked-in environment are needed. The script carries the small amount of context it needs to execute.

uv add --script script.py "httpx>=0.27"
uv remove --script script.py requests
uv lock --script script.py

uv updates the embedded metadata. The last command writes a lockfile next to the script when reproducibility matters — it is not embedded in the Python file. For script.py, the file is script.py.lock.

Here is the opening of the lockfile generated by uv 0.12.9 in the benchmark environment. The rest contains every resolved package, URL, hash, and compatible wheel:

# script.py.lock
version = 1
revision = 3
requires-python = ">=3.12"

[manifest]
requirements = [
  { name = "requests", specifier = "<3" },
  { name = "rich" },
]

[[package]]
name = "certifi"
version = "2026.7.22"
source = { registry = "https://pypi.org/simple" }
# ... hashes and wheels omitted

Keeping the declaration inline makes the script portable; keeping the resolved graph beside it makes exact runs repeatable and reviewable.

Project or inline?

PEP 723 does not make uv intrinsically faster or slower. It changes where the declaration lives. uv manages a project in a .venv; for a script with inline metadata, it creates a dedicated environment in its cache. Both paths use uv's resolver and aggressive dependency cache.

Style Declaration First run Repeat run
Normal uv project pyproject.toml + project-local .venv 1.55 s 0.16 s
PEP 723 script # /// script metadata + cached script environment 1.18 s 0.16 s

The PEP 723 form is faster in this comparison's first run; once ready, both complete in 0.16 s. That is not a general performance property of PEP 723: it is a metadata format, not a different resolver. First-run time includes creating a different environment layout and retrieving dependencies. A project maintains uv.lock automatically; lock an inline script explicitly with uv lock --script script.py. Choose the normal project form for shared configuration, modules, tests, and tooling; choose PEP 723 for a portable, self-contained utility.

# Normal uv project (from its project directory)
uv run script.py

# PEP 723 inline script
uv run --script script.py

python script.py avoids runner overhead only when its dependencies are already installed in the selected environment; inline metadata lets the file carry the declarative setup instruction with it.

This fits small automations, data pulls, maintenance tools, and one-off reports. A multi-module application still benefits from a normal project file.

References

fullscreen