Skip to content(if available)orjump to list(if available)

Uv: Running a script with dependencies

simonw

The "declaring script dependencies" thing is incredibly useful: https://docs.astral.sh/uv/guides/scripts/#declaring-script-d...

  # /// script
  # dependencies = [
  #   "requests<3",
  #   "rich",
  # ]
  # ///
  import requests, rich
  # ... script goes here
Save that as script.py and you can use "uv run script.py" to run it with the specified dependencies, magically installed into a temporary virtual environment without you having to think about them at all.

It's an implementation of Python PEP 723: https://peps.python.org/pep-0723/

Claude 4 actually knows about this trick, which means you can ask it to write you a Python script "with inline script dependencies" and it will do the right thing, e.g. https://claude.ai/share/1217b467-d273-40d0-9699-f6a38113f045 - the prompt there was:

  Write a Python script with inline script
  dependencies that uses httpx and click to
  download a large file and show a progress bar
Prior to Claude 4 I had a custom Claude project that included special instructions on how to do this, but that's not necessary any more: https://simonwillison.net/2024/Dec/19/one-shot-python-tools/

slt2021

shebang mode is also incredibly useful and allows execution like ./script.sh

  #!/usr/bin/env -S uv run --script
  # /// script
  # dependencies = [
  #   "requests<3",
  #   "rich",
  # ]
  # ///
  import requests, rich
  # ... script goes here

meander_water

PEP723 is also supported by pipx and hatch, even though uv is (deservedly) getting all the attention these days.

Others like pip-tools have support in the roadmap (https://github.com/jazzband/pip-tools/issues/2027)

minimaxir

I thought that was a heart emoticon next to requests, for a second.

tayloramurphy

I mean, who doesn't love requests?

tczMUFlmoNk

Well... maybe the people who have stopped working on it due to the mysterious disappearance of 30,000 fundraised dollars, selling paid support but "delegat[ing] the actual work to unpaid volunteers", and a pattern of other issues from other community members who have not spoken up about them.

https://vorpus.org/blog/why-im-not-collaborating-with-kennet...

https://news.ycombinator.com/item?id=19826680

JimDabell

It stopped accepting new features a decade ago, it doesn’t support HTTP/2, let alone HTTP/3, it doesn’t support async, and the maintainers ignored the latest security vulnerability for eight months.

It was good when it was new but it’s dangerously unmaintained today and nobody should be using it any more. Use niquests, httpx, or aiohttp. Niquests has a compatible API if you need a drop-in replacement.

js2

I've noticed that Claude Code prefers httpx because it's typed.

skissane

This is cool, but honestly I wish it was builtin language syntax not a magic comment, magic comments are kind of ugly. Maybe some day…

(I realise there are some architectural issues with making it built-in syntax-magic comments are easier for external tools to parse, whereas the Python core has very limited knowledge of packaging and dependencies… still, one of these days…)

freddie_mercury

It IS built-in language syntax. It's defined in the PEP, that's built-in. It's syntax:

"Any Python script may have top-level comment blocks that MUST start with the line # /// TYPE where TYPE determines how to process the content. That is: a single #, followed by a single space, followed by three forward slashes, followed by a single space, followed by the type of metadata. Block MUST end with the line # ///. That is: a single #, followed by a single space, followed by three forward slashes. The TYPE MUST only consist of ASCII letters, numbers and hyphens."

That's the syntax.

Built-in language syntax.

skissane

Suppose I have such a begin block without the correct corresponding end block - will Python itself give me a syntax error, or will it just ignore it?

It might be “built-in syntax” from a specification viewpoint, but does CPython itself know anything about it? Does CPython’s parser reject scripts which violate this specification?

And even if CPython does know about it (or comes to do so in the future), the fact that it looks like a comment makes its status as “built-in syntax” non-obvious to the uninitiated

frollogaston

Funny how all this time, the only commonly-used import syntax like this was in HTML+JS with the <script> tag.

ImJasonH

This is cool!

This gave me the questionable idea of doing the same sort of thing for Go: https://github.com/imjasonh/gos

(Not necessarily endorsing, I was just curious to see how it would go, and it worked out okay!)

hoherd

One gotcha I caught myself in with this technique is using it in a script that would remediate a situation where my home has lost internet and needed the router to be power cycled. When the internet is out, `uv` cannot download the dependencies specified in the script, and the script would fail. Thankfully I noticed this problem after writing it but before needing it to actually work, and refactored my setup to pre-install the needed dependencies. But don't make the same mistake I almost made! Don't use this for code that may need to run airgapped! Even with uv caching you may still get a cache miss.

indigodaddy

But don't you have to only ever run it once to have the deps/venv for subsequent runs?

do_not_redeem

I don't know if uv garbage collects its cache, but it wouldn't surprise me. Otherwise disk usage would grow indefinitely.

gopalv

This is my absolute favourite uv features and the reason I switched to uv.

I have a bunch of scripts in my git-hooks which have dependencies which I don't want in my main venv.

#!/usr/bin/env -S uv run --script --python 3.13

This single feature meant that I could use the dependencies without making its own venv, but just include "brew install uv" as instructions to the devs.

alkh

If I may interject, does anyone have any idea why the `-S` flag is required? Testing this out on my machine with a BSD env, `/usr/bin/env -S uv run --python 3.11 python` and `/usr/bin/env uv run --python 3.11 python` do the same thing (launch a Python interactive shell). Man page for env doesn't clarify a lot to me, as the end result still seems to be the same("Split apart the given string into multiple strings, and process each of the resulting strings as separate arguments to the env utility...")

Skunkleton

Its needed on my ubuntu 24.04 system

  $ cat test.sh
  #!/usr/bin/env bash -c "echo hello"
  $ ./test.sh
  /usr/bin/env: ‘bash -c "echo hello"’: No such file or directory
  /usr/bin/env: use -[v]S to pass options in shebang lines
  $ ./test.sh # with -S
  hello

alkh

Interesting, I guess it is indeed a BSD thing, cause this works for me with or without '-S' on Mac

Hello71

alkh

Thanks, but the main question is how come the behaviour is still the same whether you pass the flag or not? I would get it if it just failed without "-S" but it works as intended. I am wondering if this is cause I might not be using the GNU version of env, so this is less relevant? Edit: looks to be the version thing indeed, this doesn't work for someone on Ubuntu

dwood_dev

Completely agree. UV for stalled what was going to be a major project to move lots of python to golang. There will still be a lot migrated, but smaller script like things are no longer in scope.

abdusco

I still write small some scripts in golang when the bootup time is important. Python still takes its time to boot up, and it's not the best tool for the job if it's gonna be called like a shell utility for thousands of files, for example.

Bluestein

I honestly believe this is a killer (killer) feature.-

AceJohnny2

Note that this only works for single-file scripts.

If you have a project with modules, and you'd like a module to declare its dependencies, this won't work. uv will only get those dependencies declared in the invoked file.

For a multi-file project, you must have a `pyproject.toml`, see https://docs.astral.sh/uv/guides/projects/#managing-dependen...

In both cases, the script/project writer can use `uv add <dependency>`, just in the single-file case they must add `--script`.

trostaft

This is pretty great. Passing python code out to my students is usually also confronted with the question of "How do I run it?", which is usually terrible to answer. Now, I can just tell them to get uv (single command) and run it.

heisenzombie

Quick plug here for a simple Jupyter kernel I created:

https://github.com/tobinjones/uvkernel

It’s a pretty minimal wrapper around “uv” and “iPython” to provide the functionality from the article, but for Jupyter notebooks. It’s similar to other projects, but I think my implementation is the least intrusive and a good “citizen” of the Jupyter ecosystem.

There’s also this work-in-progress:

https://github.com/tobinjones/pep723widget

Which provides a companion Jupyter plugin to manage the embedded script dependencies of noteboooks with a UI. Warning — this one is partially vibe-coded and very early days.

rented_mule

There is also marimo. For my use cases it has been a fantastic upgrade from Jupyter. I use it in the style of TFA (notebooks are just Python files). In the notebook UI, you can add packages to the script headers. It's so nice to have many notebooks in one directory with independent, but reproducible environments.

Marimo notebooks are easy to diff when committing to git. Plus you get AI coding assistants in the notebook, a reactive UI framework, the ability to run in the browser with Pyodide on WASM, and more. It will also translate your old Jupyter notebooks for you.

For me, what uv is to package managers, marimo is to notebooks.

https://docs.astral.sh/uv/guides/integration/marimo/

m4r71n

Oh nice, I was already a happy user of the uv-specific shebang with in-script dependencies, but the `uv lock --script example.py` command to create a lock file that is specific to one script takes it to another level! Amazing how this feels so natural and yet only appeared after 20+ years of Python packaging.

billyjmc

What’s your use case for locking dependencies on a single script?

One things that’s useful to my organization is that we can then proceed to scan the lockfile’s declared dependencies with, e.g., `trivy fs uv.lock` to make sure we’re not running code with known CVEs.

SafeDusk

I am using this feature in my MCP servers and it is a god send! Some details on how I do it can be found at https://blog.toolkami.com/toolkami-shttp-server/.

bb88

There's a lot of small one off things where deploying uv and the script makes it easy to use. A lot of use cases for golang can be replaced with uv.

rr808

How many package managers can one language have? Its a simple language but setting it up is just incredibly bad. Maybe this is the one or should I wait for the next?

selcuka

I had the same sentiment, but uv seems to have eliminated the competition. Installing uv using your OS package manager is enough as it can also download and install (isolated) Python interpreters as well.

frollogaston

I wish they would make uv default and end this game.

xyse53

I recommend trying it, it gets a ton of hype but I think for good reason.

This is the one.

roywiggins

imo all I usually need is pip-tools and venv, but uv kind of bundles the two together in a very natural way (and is very fast)

nomel

Unless you're installing a new version of python, I have trouble seeing how preceding all the normal command with "uv" can be seen as much of a difference.

satertek

Why doesn't pip support PEP 723? I'm all for spreading the love of our lord and savior uv, but it should be necessary to have an official implementation.

simonw

I don't know if there's an official reason, but my guess is that it's slightly out of scope for pip: pip is a packaging installation tool, not a virtual environment managing system.

uv, being both, is a more natural fit for an implementation of that PEP.

Here's a relevant discussion: https://discuss.python.org/t/idea-introduce-standardize-proj...

ayerajath

https://docs.astral.sh/uv/guides/scripts/#declaring-script-d...

this is neat af. my throw-away scripts folder will be cleaner now.

staplung

Love this feature of UV. Here's a one-liner to launch jupyter notebook without even "installing" it:

  uv run --with jupyter jupyter notebook
Everything is put into a temporary virtual environment that's cleaned up afterwards. Best thing is that if you run it from a project it will pick up those dependencies as well.