Snippets: Python and the uv Shebang

Ok, everyone’s doing posts on the uv
shebang trick so I have to, too. But no actually I really do want to cover this killer feature of uv
because it finally gives us the python “scripts” that we were promised.
Some quick background: uv
is a super useful tool that (among other things) provides a much more pleasant way to manage Python versions and dependencies. It’s kinda like pip
, poetry
, pyenv
, and pipx
all rolled into one. And it’s scary fast. AND stupid easy to install. Do a quick search and you’ll see lots of folks evangelizing this thing.
The fundamental problem I’m covering here is when you want to write, run, and distribute a Python script. If you have any external dependencies, then you have to somehow package that information along with your Python script, typically in the form a requirements.txt
. When you’re working with other developers, this is inconvenient (to say the least) since everyone seems to have a different way of managing Python versions and dependencies.
Besides uv
, the other two critical parts to this trick are things you probably already know about:
- The “shebang” line. You can tell the shell how to execute the contents of the file. The
-S
flag is important below, but you can read about it on your own. - PEP-723 is an inline script metadata specification. You can tell tools (like
uv
) what dependencies your script has within the file itself.
I’ll cut to the chase: you can wire these up so that your script will be run with uv
, which will install any necessary deps before running the script. Because uv
is so freakishly fast, this works utterly seamlessly. Here’s an example snippet:
#!/usr/bin/env -S uv run --script
# /// script
# requires-python = ">=3.12"
# dependencies = [
# "click",
# ]
# ///
import click
@click.group()
def cli():
pass
@cli.command()
@click.argument("s")
def echo(s: str):
"""Echo the input."""
click.secho(s)
if __name__=="__main__":
cli()
Finally, you can do chmod u+x myscript.py
and you’re off to the races with ./myscript.py
. The only remaining wrinkle is that if you’re actively developing, you’ll probably still want to create a .venv
with the necessary dependencies so your IDE can do its magic (AFAIK vscode doesn’t leverage inline script metadata but that may change). How you do that is totally up to you but it’s pretty trivial with uv init
, uv add click
, and uv sync
.
Alright, that’s all for this one, happy hacking!