This document describes uv's support for single-file Python scripts with inline dependency metadata, following PEP 723. Script execution enables running Python files with dependencies specified directly within the script, without requiring a full project structure or separate requirements.txt files.
For information about running commands in projects with pyproject.toml files, see Project Commands. For general Python environment management, see Python Environment Management.
uv's script execution system provides three primary capabilities:
uv.lock files for scripts to ensure reproducible dependency resolutionScripts can be executed from local files, stdin, or remote URLs. The system automatically handles environment creation, dependency installation, and command execution.
Sources: crates/uv/src/commands/project/run.rs1-1500
PEP 723 defines a standard for embedding TOML metadata within Python script comments. The metadata block must:
# /// script on a line by itself#)# /// on a line by itselfThe metadata follows a TOML schema with these key fields:
dependencies: Array of PEP 508 dependency specifiersrequires-python: Version specifier for Python compatibilitytool.uv.*: uv-specific configuration (indexes, sources, constraints, etc.)Example:
Sources: crates/uv-scripts/src/lib.rs158-266
Diagram: PEP 723 Metadata Parsing Pipeline
Sources: crates/uv-scripts/src/lib.rs21-266 crates/uv-scripts/src/lib.rs410-500
| Structure | Purpose | Location |
|---|---|---|
Pep723Item | Enum for script types (Script/Stdin/Remote) | crates/uv-scripts/src/lib.rs23-69 |
Pep723Script | Script with path and parsed metadata | crates/uv-scripts/src/lib.rs159-169 |
Pep723Metadata | Parsed TOML metadata structure | crates/uv-scripts/src/lib.rs401-500 |
Pep723ItemRef<'item> | Borrowed reference to script | crates/uv-scripts/src/lib.rs72-150 |
ScriptTag | Raw metadata block extraction | crates/uv-scripts/src/lib.rs410-500 |
Sources: crates/uv-scripts/src/lib.rs1-500
uv supports three sources for Python scripts:
Scripts read from the local filesystem. These are the most common case and support all features including lockfile generation.
Key Functions:
Pep723Script::read(): Reads and parses script from file crates/uv-scripts/src/lib.rs177-200ScriptInterpreter::root(): Determines cache location crates/uv/src/commands/project/mod.rs580-658Scripts provided via standard input. These create ephemeral environments and do not support lockfiles.
The system prevents conflicts when both script and requirements are read from stdin crates/uv/src/commands/project/run.rs152-159
Scripts fetched from remote URLs (e.g., GitHub Gists). Environments are cached based on URL hash.
GitHub Gist Support: The system includes special handling for GitHub Gists crates/uv/src/commands/project/run.rs49-58:
Sources: crates/uv/src/commands/project/run.rs209-227 crates/uv-scripts/src/lib.rs23-69
Script environments are isolated Python virtual environments created specifically for a script's dependencies. The system uses two strategies: cached environments (for scripts with metadata) and ephemeral environments (for scripts without metadata).
Diagram: Script Environment Resolution Flow
Sources: crates/uv/src/commands/project/mod.rs574-790 crates/uv/src/commands/project/run.rs208-509
The ScriptInterpreter::root() function determines where environments are stored crates/uv/src/commands/project/mod.rs580-658:
| Script Type | Cache Key | Path Pattern |
|---|---|---|
| Local Script | cache_digest(&script.path) | <cache>/environments/<name>-<digest>/ |
| Remote URL | cache_digest(url) | <cache>/environments/<digest>/ |
| Stdin | cache_digest(&metadata.raw) | <cache>/environments/<digest>/ |
The function also respects VIRTUAL_ENV when --active is used, allowing scripts to run in the active environment crates/uv/src/commands/project/mod.rs627-654
The environment_is_usable() function checks whether an existing environment can be reused crates/uv/src/commands/project/mod.rs825-900:
Validation Checks:
pyvenv.cfg configurationpython_request if providedrequires-python from metadataSources: crates/uv/src/commands/project/mod.rs825-900
The ScriptInterpreter enum represents either a new interpreter or an existing environment:
The ScriptInterpreter::discover() method crates/uv/src/commands/project/mod.rs661-751:
ScriptInterpreter::root()PythonEnvironment::from_root()environment_is_usable()Environment (reuse) or Interpreter (create new)Sources: crates/uv/src/commands/project/mod.rs564-790
Scripts can optionally have lockfiles (uv.lock) for reproducible dependency resolution. Lockfiles are stored adjacent to the script with the pattern <script>.lock or in a .uv/ directory.
The system checks for existing lockfiles using LockTarget::from() crates/uv/src/commands/project/lock_target.rs:
When a lockfile exists, the script execution flow includes locking and syncing crates/uv/src/commands/project/run.rs229-358
Diagram: Script Lockfile Resolution Flow
Sources: crates/uv/src/commands/project/run.rs229-358 crates/uv/src/commands/project/lock.rs81-253
Scripts support three lock modes crates/uv/src/commands/project/lock.rs255-265:
| Mode | Description | When Used |
|---|---|---|
Frozen | Use existing lock without validation | --frozen flag |
Locked(interpreter, source) | Validate lock is current, error if not | --locked or --check-locked |
Write(interpreter) | Update lock if needed | Default behavior |
The lock mode is determined in run.rs crates/uv/src/commands/project/run.rs264-271:
For local scripts, lockfiles are stored using LockTarget::lock_path():
<script-dir>/<script-name>.lock or <script-dir>/.uv/<script-name>.lockscript.py → script.py.lock or .uv/script.py.lockSources: crates/uv/src/commands/project/run.rs229-358 crates/uv/src/commands/project/lock.rs255-433
Scripts support adding and removing dependencies through the uv add and uv remove commands, which modify the inline metadata.
The add command handles scripts via AddTarget::Script crates/uv/src/commands/project/add.rs178-245:
Add Flow:
ScriptInterpreter::discover() crates/uv/src/commands/project/add.rs228-243PyProjectTomlMut crates/uv/src/commands/project/add.rs517-900The remove command uses RemoveTarget::Script crates/uv/src/commands/project/remove.rs66-283:
Remove Flow:
PyProjectTomlMut crates/uv/src/commands/project/remove.rs116-123Both operations use PyProjectTomlMut with DependencyTarget::Script to preserve formatting crates/uv/src/commands/project/add.rs117:
This ensures comments and structure are preserved when modifying dependencies.
Sources: crates/uv/src/commands/project/add.rs178-1100 crates/uv/src/commands/project/remove.rs66-448
The complete script execution flow integrates all components from parsing to running the command.
Diagram: Complete Script Execution Pipeline
Sources: crates/uv/src/commands/project/run.rs80-1400
The run() function is the main entry point crates/uv/src/commands/project/run.rs80-113:
Key Parameters:
script: The PEP 723 item if detectedcommand: The command to execute (or Python stdin/gui)requirements: Additional --with requirementsfrozen, lock_check: Lockfile behavior controlsThe script is detected and parsed before the run() call. Within run(), the system handles three paths crates/uv/src/commands/project/run.rs210-509:
For scripts with metadata, ScriptEnvironment::get_or_init() handles environment creation crates/uv/src/commands/project/environment.rs:
This function:
ScriptInterpreter::root()--with)Scripts support --with for additional dependencies crates/uv/src/commands/project/run.rs955-1100:
These create an overlay environment via .pth files crates/uv/src/commands/project/run.rs1100-1250:
--with deps to cached environment.pth file pointing to cached site-packagessys.pathFinally, the command is executed via run_to_completion() crates/uv/src/commands/project/run.rs1350-1400:
This spawns the process with:
PATH to include script environment's bin directoryVIRTUAL_ENV environment variableSources: crates/uv/src/commands/project/run.rs80-1400 crates/uv/src/child.rs
Script execution integrates with several other uv commands beyond uv run.
| Command | Script Support | Behavior |
|---|---|---|
uv run | Full | Execute script with dependencies |
uv add | Full | Add dependencies to inline metadata |
uv remove | Full | Remove dependencies from inline metadata |
uv sync | Full | Sync script environment with lockfile |
uv lock | Full | Generate/update lockfile for script |
uv tree | Full | Show dependency tree for script |
uv export | Full | Export requirements from script |
The uv add and uv lock commands can initialize PEP 723 metadata if it doesn't exist crates/uv/src/commands/project/add.rs206-220:
This uses Pep723Script::init_metadata() to generate default metadata crates/uv-scripts/src/lib.rs222-266:
dependencies = []requires-python if specifiedScripts can be exported to various formats via uv export crates/uv/src/commands/project/export.rs58-461:
Supported formats:
requirements.txt format with --format requirements-txtpylock.toml format with --format pylockSources: crates/uv/src/commands/project/add.rs178-245 crates/uv/src/commands/project/lock.rs82-253 crates/uv/src/commands/project/export.rs37-461
Script execution includes protection against infinite recursion when scripts use shebangs like #!/usr/bin/env -S uv run.
The system tracks recursion depth via environment variable crates/uv/src/commands/project/run.rs114-127:
Implementation Details:
UV_INTERNAL__RECURSION_DEPTHmax_recursion_depth parameter)uv run invocation--script flagThis prevents infinite loops when:
#!/usr/bin/env -S uv run./script.pyuv run script.pyuv run executes script, which triggers shebang againThe --script flag bypasses this by explicitly marking script execution mode.
Refresh this wiki