Single-file scripts with dependencies
Declare a script's Python version and dependencies in a comment block at the top of the file, and run it with uv without creating a project or environment yourself.
What you will learn
- Write a PEP 723 "# /// script" metadata block
- Run a script that has dependencies with uv run
- Read the metadata block from Python with tomllib
- Decide when a script should become a project
On this page
A useful script of forty lines should not need a project folder, a pyproject.toml and a virtual environment. Yet as soon as it imports a third-party package, it needs all three, or a note that says “first run pip install humanize”. PEP 723 fixes this by letting the script carry its own requirements in a specially formatted comment.
The metadata block
Here is a script that prints file sizes in a human-readable way, using the humanize package:
# /// script# requires-python = ">=3.11"# dependencies = [# "humanize",# ]# ///import sys
import humanize
for arg in sys.argv[1:]: print(humanize.naturalsize(int(arg)))The block starts at a line that is exactly # /// script and ends at a line that is exactly # ///. Every line in between starts with #, and the text, once the # prefix is removed, is TOML. The two keys you will use most are the ones from pyproject.toml: requires-python and dependencies. For Python itself the block is a comment, so a plain interpreter ignores it.
Running it with uv
Tools that understand the block can read it, make a temporary environment with the right packages, and run the script in it. uv run does this:
$ uv run sizes.py 1500 3000000Installed 1 package in ...1.5 kB3.0 MBThere was no venv, no pip and no activation. The environment is cached, so the second run starts immediately, without the install line:
$ uv run sizes.py 15001.5 kBpipx run sizes.py reads the same block. Because the block is a standard, any tool can support it.
Running the same file with plain python shows what the block does and does not do. It declares requirements, it does not install them:
$ python sizes.py 5Traceback (most recent call last): File "sizes.py", line 9, in <module> import humanizeModuleNotFoundError: No module named 'humanize'To add a dependency without editing the comment by hand, uv can do it for you:
$ uv add --script sizes.py richOn macOS and Linux you can go one step further and make the script directly executable. Put this as the very first line, above the metadata block, mark the file executable with chmod +x sizes.py, and run it as ./sizes.py:
#!/usr/bin/env -S uv run --scriptThe block is just TOML
Because the content is TOML, you can read it with tomllib from the lesson before. This script reads its own block, which shows exactly what a tool sees:
# /// script# requires-python = ">=3.11"# dependencies = [# "humanize",# "rich>=13",# ]# ///import tomllibfrom pathlib import Path
def read_script_block(source): """Return the TOML inside the '# /// script' block as a dict, or None.""" lines = source.splitlines() if "# /// script" not in lines: return None start = lines.index("# /// script") + 1 body = [] for line in lines[start:]: if line == "# ///": break body.append(line.removeprefix("# ").removeprefix("#")) return tomllib.loads("\n".join(body))
metadata = read_script_block(Path(__file__).read_text(encoding="utf-8"))print("requires-python:", metadata["requires-python"])for requirement in metadata["dependencies"]: print("needs", requirement)requires-python: >=3.11needs humanizeneeds rich>=13This version is deliberately simple. It ignores details the standard allows, such as several blocks in one file with different types, and it is meant for understanding, not for production. A real tool uses a stricter parser.
When to stop using a script
Inline metadata suits one file that you run, share by email, or keep in a bin folder. Once you have two files that import each other, tests, or a command you want to install for other people, you are describing a project, and the pyproject.toml of the previous lesson is the right tool. The move is mechanical, because the keys are the same.
Which of these blocks will not work as intended, and why?
Expected: one line per block, in order, reading works or broken: <symptom>.
# /// script# dependencies = ["requests"]# ///# /// scriptdependencies = ["requests"]# ///#/// script# dependencies = ["requests"]# ///Show solution
The second and the third.
In the second, the middle line does not start with #, so the block has no valid end. Every line between the opening and closing tags must be a comment, and uv reports an opening tag without a closing tag.
In the third, the opening line is #/// script, with no space after #, so it does not match the required # /// script. Nothing complains: the block is not recognised, no dependencies are installed, and the script fails later with ModuleNotFoundError.
The first is valid. requires-python is optional.
Here is a script that works when humanize is installed. Add a metadata block that declares the dependency and a minimum Python of 3.11, so that uv run can run it from a clean machine. It should print 1,234,567.
import humanize
print(humanize.intcomma(1234567))Show solution
# /// script# requires-python = ">=3.11"# dependencies = ["humanize"]# ///import humanize
print(humanize.intcomma(1234567))$ uv run commas.py1,234,567A list on one line is fine in TOML. The block must come before the first line of code that is not a comment or blank line, which here is the import.
Write a script called shout.py that takes words from the command line and prints them with rich markup, for example uv run shout.py hello world printing hello world in bold. Declare rich in the metadata, and add requires-python. Run it with uv. Then run it with plain python outside any environment where rich is installed and read the error.
Expected: uv run shout.py hello world prints hello world in bold, and the last line of the plain python run is:
ModuleNotFoundError: No module named 'rich'Show solution
# /// script# requires-python = ">=3.11"# dependencies = ["rich"]# ///import sys
from rich import print
print("[bold]" + " ".join(sys.argv[1:]) + "[/bold]")$ uv run shout.py hello worldhello world$ python shout.py hello worldModuleNotFoundError: No module named 'rich'The first command prints the words in bold in a colour-capable terminal. The second shows that the block declares requirements and does not install them: only a tool that reads it, such as uv or pipx, builds the environment.
Summary
- A
# /// scriptcomment block at the top of a file holds TOML withrequires-pythonanddependencies(PEP 723). uv run script.py(orpipx run script.py) builds a cached environment from the block and runs the script. Plainpythonignores it.- Move to a
pyproject.tomlproject when one file becomes several, or when you want tests and installable commands.