Lesson: UV Script Dependencies for Single-File Tools
Context
When creating single-file Python tools/scripts that have external dependencies, especially when:
- The script needs to be easily shareable
- Dependencies need to be explicitly declared
- You want reproducible execution environments
- The script is part of a larger project but has its own dependencies
Context
When creating single-file Python tools/scripts that have external dependencies, especially when:
- The script needs to be easily shareable
- Dependencies need to be explicitly declared
- You want reproducible execution environments
- The script is part of a larger project but has its own dependencies
Problem
Scripts with external dependencies often fail when shared because dependencies aren’t explicitly declared, leading to:
- ModuleNotFoundError when dependencies aren’t installed
- Version conflicts when constraints aren’t specified
- Failures when project-level dependencies aren’t available
- Time wasted on manual environment setup
Constraint
Always use uv’s inline script metadata to declare dependencies for standalone scripts. The metadata must:
- List all external dependencies with version constraints
- Specify required Python version if needed
- Use
exclude-newer
for reproducibility when appropriate- If we cannot find a version for some newer package, we must adjust the
exclude-newer
date to a later date or remove it - Must be a full date-time string in RFC 3339 format
- If we cannot find a version for some newer package, we must adjust the
- Be managed using
uv add --script
rather than manual editing
Explanation
uv’s inline script metadata provides a standardized way to declare dependencies that lives with the script itself. This ensures:
- Dependencies are automatically installed in an isolated environment
- Version constraints are respected
- Python version requirements are enforced
- The script works reliably outside project contexts
Examples
Incorrect Approach
# script.py - Will fail if dependencies aren't installed
import requests
from rich.progress import track
import pandas as pd
def process_data(url):
resp = requests.get(url)
data = resp.json()
df = pd.DataFrame(data)
for row in track(df.itertuples()):
# ... processing ...
Correct Approach
#!/usr/bin/env -S uv run
# /// script
# requires-python = ">=3.10"
# dependencies = [
# "requests<3",
# "rich>=13.0.0",
# "pandas>=2.0.0",
# "gptme @ git+https://github.com/ErikBjare/gptme.git",
# ]
# [tool.uv]
# exclude-newer = "2025-01-01T00:00:00Z"
# ///
"""
Tool for processing data.
Usage:
./tool.py
"""
import requests
from rich.progress import track
import pandas as pd
def process_data(url):
resp = requests.get(url)
data = resp.json()
df = pd.DataFrame(data)
for row in track(df.itertuples()):
# ... processing with gptme ...
The script can be made executable and run directly:
chmod +x tool.py
./tool.py
The shebang line #!/usr/bin/env -S uv run
combined with the script metadata allows uv to automatically manage dependencies. This is especially useful for:
- Command-line tools and utilities
- Long-running scripts and daemons
- Scripts that need to be executable from other programs
Origin
- Date: 2024-12-20
- Source: uv documentation and experience with script distribution
- Fixed by: Using uv’s inline script metadata system
Notes
- Always specify version constraints for reliability
- Keep dependencies minimal and self-contained
- Consider using
exclude-newer
for reproducible builds - Test with the specified Python version