Self-contained python scripts with uv
Lately, I’ve been attacked with Rust propaganda promising us a bright new future for software people. I made two attemps to learn rust, but it was not needed to use the excelent suite of tools that rust programmers are building for the rest of us. This is especially notable with all the software rewrites in Rust that are actually delivering interesting results. One of these Rust-based tools is uv, a Python package manager that’s been catching my attention.
I’ve been using Python since 2016 for pretty much everything, small projects, servers, a wordle game, data analysis, data scraping, and especially those quick scripts that solve a specific problem. In these 9 years, the worst part were fighting with pip, installing and handling pip environments and dependencies. Every single time, and usually multiple times, when I needed to use the same script in a different machine.
UV lets you read dependencies directly from the Python file itself. Now, this isn’t some magical uv only feature, it is actually part of PEP 723, so it is becoming a standard thing. But here’s what makes uv exceptionally comfortable: it also handles the Python installation for you. I don’t need to look for my python path or version again, or choose about ‘python’, ‘python3’, ‘python3.10’, ‘python3.exe’ etc.
PEP 723
PEP 723 is a Python standard that allows you to specify the dependencies of a Python script within the script itself. From the abstract on its own page:
This PEP specifies a metadata format that can be embedded in single-file Python scripts to assist launchers, IDEs and other external tools which may need to interact with such scripts.
Great! I love it. No more creating requirements.txt
files or setting up entire PyCharm projects for a simple script. I can just write my code and specify the Python version and dependencies right there in the file. It’s beautiful.
How to do it
Let’s try the example shown on the PEP page:
# /// script
# requires-python = ">=3.11"
# 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([(k, v["title"]) for k, v in data.items()][:10])
And by running the script with uv:
$ uv run file.py
Installed 9 packages in 316ms
[
('1', 'PEP Purpose and Guidelines'),
('2', 'Procedure for Adding New Modules'),
('3', 'Guidelines for Handling Bug Reports'),
('4', 'Deprecation of Standard Modules'),
('5', 'Guidelines for Language Evolution'),
('6', 'Bug Fix Releases'),
('7', 'Style Guide for C Code'),
('8', 'Style Guide for Python Code'),
('9', 'Sample Plaintext PEP Template'),
('10', 'Voting Guidelines')
]
No pip, no venv, no “wait, where is the requierements.txt?”, I don’t have the risk of running pip install outside an environment. Just uv run
. That’s it.
Making executable bash scripts with Python
I use Python for scripts because I hate bash. With what I’ve shown above, we can take this even further and create a Python file that runs like any other executable, just ./foo.py
and you’re done.
To do this, we just need to add a shebang at the top of the file:
#!/usr/bin/env -S uv run --script
Broken down, this is what’s happening:
#!
is a shebang; it indicates that the rest of the line is the command that must be executed/usr/bin/env
attempts to find the executable in the PATH-S
is a flag that tellsenv
to split the rest of the line into argumentsuv run
is the command that will be executed--script
is a flag that tellsuv
to read the first lines of the script as dependencies
And when you run ./foo.py
, it handles all the dependency and Python version magic automatically. Same output as before, but now it is a proper executable script.
Running it online
If all of this wasn’t enough, you can also run scripts that live online. Check this out:
$ uv run https://raw.githubusercontent.com/MRoblesR/prueba/refs/heads/main/main.py
Installed 9 packages in 316ms
[
('1', 'PEP Purpose and Guidelines'),
('2', 'Procedure for Adding New Modules'),
('3', 'Guidelines for Handling Bug Reports'),
('4', 'Deprecation of Standard Modules'),
('5', 'Guidelines for Language Evolution'),
('6', 'Bug Fix Releases'),
('7', 'Style Guide for C Code'),
('8', 'Style Guide for Python Code'),
('9', 'Sample Plaintext PEP Template'),
('10', 'Voting Guidelines')
]
It downloads the file, installs the dependencies, and runs it. Previously, you had to work with curl
and bash
scripts. Now, it’s possible with Python scripts.
DO NOT RUN ONLINE SCRIPTS FROM RANDOM LINKS YOU DON’T TRUST. Even tough it’s a extremely comfortable feature, it’s also a huge security risk.
Conclusion
We can create a Python file, define its dependencies right inside it, and run it as a regular executable script without even having Python installed on the system. This is genuinely a huge step forward in fixing the most annoying problems with Python scripting.
Honestly? UV is what pip should have been from day one.