uv is an “extremely fast Python package and project manager, written in Rust”.
It took off last year as an alternative for previous python package managers such as pip
, pip-tools
, poetry
, and virtualenv
.
The coolest thing about UV, apart from the fact that it’s really fast and makes managing python virtually a solved problem, is that you can use it as a way to run python scripts in 0 steps!
If you had a python script, you’d need at least one pre-flight step before you could run it on a new machine:
- install all dependencies
- (can include python itself, or libraries, or maybe even your preferred package manager itself)
- run the script
uv, however, when combined with inline script metadata (PEP 723) and uv Scripts , can turn any single-file script into a fully portable executable (kind of like pex but not really).
With a clever polyglot trick—a single file written to be valid in multiple languages (in this case, shell and Python)—we can also make uv
self-install, so we don’t even need to have it beforehand!
Embed python and library versions on the script itself
Inline script metadata can be used to embed any and all dependencies for a script in the script itself:
# /// script# requires-python = "~=3.12"# dependencies = [# "marimo==0.11.25",# "numpy",# "requests",# ]# ///
If you include the commented snippet above anywhere in your code, PEP 723 tells us that it will be identified and parsed to identify python version 3.12 and these marimo, numpy and requests dependencies.
uv can now resolve any dependency and execute your script:
uv run --script my_script.py
Turning the script into an executable file
We can also use a shebang added at the top of the file to make it easier.
#!/usr/bin/env -S uv run --script
now, we can just do either way to execute it:
./my_script.py # if you did `chmod +x my_script.py`sh my_script.py # if you didn't
the problem is: we’re still depending on having uv pre-installed!
install uv, python and dependencies in zero steps and run the script with a polyglot trick
uv
itself is also really easy to install:
curl -LsSf https://astral.sh/uv/install.sh | sh
powershell -ExecutionPolicy ByPass -c "irm https://astral.sh/uv/install.ps1 | iex"
Beyond the python and library versions, we can also embed the uv installation on the script itself:
#!/usr/bin/env sh
""":"# Polyglot trick: Python sees a triple-quoted string, bash sees "" then ":" (no-op)# Install UV if needed (zero-dependency setup) - redirect output to stderr to preserve clean stdoutwhich uv >/dev/null || { echo ">>> uv install required" >&2 \ && curl -LsSf https://astral.sh/uv/install.sh | sh >&2 \ && echo ">>> uv install done\n\n" >&2}
# Execute with UV, replacing shell process (preserves stdio)exec uv run --script --quiet "$0" "$@"":"""
Key differences here:
- we turned the shebang back into bash
- we included a command on the file itself to check for uv and install if missing
- then a command to use uv to run the file with the chosen versions for python and libs
The same file is doing three things at once:
- installing uv if needed
- invoking uv
- hosting python code
putting it all together
#!/usr/bin/env sh
""":"# Polyglot trick: Python sees a triple-quoted string, bash sees "" then ":" (no-op)# Install UV if needed (zero-dependency setup) - redirect output to stderr to preserve clean stdoutwhich uv >/dev/null || { echo ">>> uv install required" >&2 \ && curl -LsSf https://astral.sh/uv/install.sh | sh >&2 \ && echo ">>> uv install done\n\n" >&2}
# Execute with UV, replacing shell process (preserves stdio)exec uv run --script --quiet "$0" "$@"":"""
# /// script# requires-python = "~=3.12"# dependencies = [# "marimo==0.11.25",# "numpy",# "requests",# ]# ///
import marimoimport numpy as npimport requests
print(f"using marimo.{marimo.__version__}")print(f"using numpy.{np.__version__}")print(f"using requests: {requests.__file__}")
# Example: Fetch authenticated datasession = requests.Session()session.cookies = requests.cookies.RequestsCookieJar()# Add your authentication logic hereprint("Script running worry-free with dependencies resolved!")
if you run sh my_script.py
on ANY brand new machine, you should see:
$ sh my_script.pyusing marimo.0.11.25using numpy.2.3.0using requests: /Users/diegolm/.cache/uv/environments-v2/py-2d879b88d0cc5794/lib/python3.12/site-packages/requests/__init__.pyScript running worry-free with dependencies resolved!
REGARDLESS of any pre-conditions.
credits
I first found this tip on Paul Willot’s blog: Standalone python script with uv
I then modified it so it were stdio-MCP-friendly (mostly, redirect stuff to sterr and preserve clean stdio)
(blog post WIP)