I create a fair few scripts in my ~/bin/
directory to automate tasks. Since discovering uv and inline script metadata, I’ve started using Python far more for these. After reading Rob Allen’s post about this technique, I’ve been using it extensively and wanted to share my experience.
As ~/bin
is on my path, I want to run the script by calling it directly on the command line. To do this, I use this shebang:
#!/usr/bin/env -S uv run --script
The command line will now run uv run --script
and pass the file as the argument. uv ignores the shebang and then runs the rest of the file as a normal Python file.
Once I’ve ensured that the script has executable permissions via chmod a+x {filename}
, I’m now good to go with simple command line scripts written in Python that automatically handle their dependencies!
Adding Dependencies
The real power comes when your Python script needs dependencies. You can declare them in a fenced code block in a leading comment and uv will parse and install those in a temporary virtual environment before invoking the script:
#!/usr/bin/env -S uv run --script
# /// script
# requires-python = ">=3.12"
# dependencies = [
# "requests",
# "rich",
# ]
# ///
import requests
from rich.console import Console
console = Console()
response = requests.get("https://api.github.com/users/octocat")
console.print(response.json())
This follows PEP 723 for inline script metadata, making your scripts completely self-contained.
The Magic of env -S
The -S
flag in the shebang line is crucial for portability. It tells env
to split the arguments properly. When the OS runs your script, it effectively executes:
/usr/bin/env '-S uv run --script' your_script.py
The -S
flag causes env
to split this back into separate arguments, making it work correctly across different systems.
Benefits
This approach gives you several advantages:
- No virtual environment management: uv handles temporary environments automatically
- Dependency isolation: Each script gets its own dependency environment
- Portability: Scripts work anywhere uv is installed
- Speed: uv’s Rust implementation makes dependency resolution and installation very fast
- Self-documenting: Dependencies are declared right in the script
Reducing Noise
If you find the “Reading inline script metadata” message annoying each time you run the script, you can add --quiet
to your shebang line:
#!/usr/bin/env -S uv run --script --quiet
Example Script
Here’s a complete example of a useful script that normalizes audio levels in video files:
#!/usr/bin/env -S uv run --script
# /// script
# requires-python = ">=3.8"
# dependencies = [
# "ffmpeg-python",
# ]
# ///
import sys
import ffmpeg
def normalize_audio(input_file, output_file):
"""Normalize audio levels in a video file."""
try:
(
ffmpeg
.input(input_file)
.output(output_file, **{'c:v': 'copy', 'filter:a': 'loudnorm'})
.overwrite_output()
.run()
)
print(f"Audio normalized: {input_file} -> {output_file}")
except ffmpeg.Error as e:
print(f"Error processing {input_file}: {e}")
sys.exit(1)
if __name__ == "__main__":
if len(sys.argv) != 3:
print("Usage: normalize_audio input.mp4 output.mp4")
sys.exit(1)
normalize_audio(sys.argv[1], sys.argv[2])
Save this as normalize_audio
, make it executable with chmod +x normalize_audio
, and you can now run it directly:
normalize_audio input.mp4 output.mp4
Compatibility Notes
The env -S
flag works on most modern Linux distributions and recent versions of macOS. However, there are some differences in how different operating systems handle shebang parsing:
- Linux: Passes the entire shebang as one argument to
env
- macOS: May split on whitespace differently
For maximum compatibility, stick to simple command lines without quotes or complex arguments in your shebang.
Getting Started
To start using this technique:
- Install uv:
curl -LsSf https://astral.sh/uv/install.sh | sh
- Create a Python script with the uv shebang line
- Add your dependencies in the script metadata block
- Make it executable:
chmod +x your_script
- Run it directly:
./your_script
This approach transforms Python scripting from a dependency management nightmare into a delightfully simple experience. No more virtual environments to manage, no more missing dependencies – just pure, portable Python scripts that work everywhere.