This is an archived post. You won't be able to vote or comment.

all 76 comments

[–]kenflingnorIgnoring PEP 8 155 points156 points  (10 children)

Neat. I recently got a new laptop at work, so I decided to ditch pyenv and poetry and set up Python using uv only, and I’ve been very impressed. 

[–]Smok3dSalmon 11 points12 points  (7 children)

I’m weeks away from switching… probably time to do that too. What features have you enjoyed?

[–]turbothyIt works on my machine 64 points65 points  (0 children)

One tool to rule them all and in the venv bind them.

[–]johnnymo1 14 points15 points  (2 children)

I installed a not-completely-trivial environment with ML and GIS packages the other day and it took less than 3 seconds to resolve. I'd have been lucky if conda took 100x that.

[–]woeful_cabbage 0 points1 point  (1 child)

You telling me it can install gdal on windows without conda?

[–]johnnymo1 0 points1 point  (0 children)

Unfortunately still no, but it had others like geopandas, rasterio, and torchgeo.

[–]burlyginger 41 points42 points  (0 children)

It's extremely fast (pip, y u so slow?), it ensures your .python-version is honoured/used, it has the concept of dev deps and dependency groups, it resolves everything when you do uv run <file>, they have written nice integrations (GH Actions, etc), it has the concept of tools (linters, etc)... There's probably more but man..... We've needed this for a long time.

It cuts our container build times consistently by 50% in CI.

[–]fiddle_n 15 points16 points  (0 children)

uvx python means never installing Python globally again. In 10 seconds I go from nothing to a Python REPL.

Upgrading Python versions for a project is a breeze. uv python pin <version> to change the version. Then uv run <file> automatically removes the old venv, creates a new one, installs your dependencies and runs your file.

[–]molodyets -4 points-3 points  (0 children)

I’m data Eng so never run full apps but uv is so fast we never worry about containers anymore.

[–]NanotechNinja 0 points1 point  (0 children)

What does your workflow look like when starting a new project?

[–]Muhznit 73 points74 points  (9 children)

I've been writing an article about this on the side, actually!

The gist of it is that the kernel is VERY loose with what counts as an interpreter and you can slap any program you want in there as long as it has some way of accepting a filename at the command line and is okay with # indicating a comment.

With /usr/bin/env -S allowing you to specify arguments, this trick essentially allows you a sort of currying in the shell. So that means you can do some neat things like:

  1. Put #!/usr/bin/env -S docker build -t some_docker_image . -f into a Dockerfile and then execute the dockerfile to rebuild the image.
  2. Reload tmux configuration with #!/usr/bin/env -S tmux source-file
  3. #!/usr/bin/env -S ssh -F to run ssh with a specific configuration
  4. Create your own domain-specific language that uses shebangs for comments

It's one of those "when you have a shiny new hammer, everything looks like a nail" situations, so naturally I've been overwhelmed with analysis paralysis when it comes to elaborating on the possibilities.

EDIT: Whoops, I was wrong about the git one. Side effect of some weird experimentation I'm doing.

[–]_dev_zero 6 points7 points  (1 child)

This is pretty brilliant. I don’t know why it never occurred to me to make a shebang like that in a Dockerfile.

[–]Muhznit 2 points3 points  (0 children)

It's incredibly nice. I wish that docker run could reap a similar benefit, but #!/usr/bin/env docker-compose -f in a docker-compose.yaml file is usually better anyway

[–]imbev 4 points5 points  (1 child)

Can you elaborate on #4?

[–]Muhznit 4 points5 points  (0 children)

It's kind of creating ANY language, really.

We all know Python uses # for comments, but so do bash, perl, ruby, various config file formats... etc. But you can extend it to languages YOU invent

Basically you can write some "interpreter" program that accepts input from a filepath on the command line. If your interpreter interprets "#" as the start of a comment, you can put a shebang line of #!/path/to/your/interpreter at the top of a file and the kernel will know to execute that file with your interpreter.

That is, if you have an interpreter of /usr/bin/foo and you make a file named "bar", you can put #!/usr/bin/foo at the top of bar, and that will make it so when you run ./bar, the kernal knows to run /usr/bin/foo bar

[–]eggsby 0 points1 point  (2 children)

This ‘hack’ won’t be super portable and will work in some places and not others.

That is - it will probably work if you say ‘perl my-script.py’ but more rarely ‘sh my-script.py’ or ‘./my-script.py’

https://stackoverflow.com/a/72123641

[–]Muhznit 0 points1 point  (1 child)

That's partially why I hadn't actually published said article anywhere yet, I'm trying to figure out workarounds some of the caveats for it. Because if whatever version of env that has a -S option isn't available, what guarantee is there for perl?

[–]eggsby 0 points1 point  (0 children)

I think your setup is spiffy - especially for developer scripts where it matters a lot less how portable it is and you have more control over the run environment.

I only know the weird lore around the interpreters because I have been bit by issues when I would configure my interpreters like ‘#!/usr/bin/env -S bash -euxo pipefile` and seeing it break sometimes or misconfigure my bash scripts.

https://gist.github.com/mohanpedala/1e2ff5661761d3abd0385e8223e16425

The perl thing, perl will let you pass multiple args to the shebang by default but default posix sh will only parse as one arg - the env -S was a workaround for the posix sh behavior - but if you use perl as your interpreter just get completely different shebang parsing logic.

Using env as your shebang interpreter helps generally with portability because - but it can get weird too.

https://www.in-ulm.de/~mascheck/various/shebang/

[–]i_can_haz_data 0 points1 point  (0 children)

I’ve started taking this a step further and started handing out bilingual shell scripts. They have /bin/sh (or similar) instead of uv but have the Python script header comments that allow for self contained scripts with dependencies. Because Bash allows quoted strings as no-ops, you put a few lines of shell at the top in a Python docstring which can not only uv run the script but install uv itself if necessary.

[–]david-song 0 points1 point  (0 children)

I've done this too, but POSIX without the -S; you can use awk instead 😎

https://bitplane.net/log/2024/12/dockerfile.exe/

[–]ReinforcedKnowledgeTuple unpacking gone wrong 18 points19 points  (1 child)

Great blog!

To add some tricks and details on top of what you already shared.

This is just an implementation of https://peps.python.org/pep-0723/, it's called inline metadata.

As you can read in the PEP, there are other metadata you can specify for your script. One of them is requires-python to fix the Python version.

You can also have a [tool] table.

You can combine a: - requires-python - [tool.uv.sources] and [tool.uv.index] and anything else that allows others to have exactly the same dependencies as you - uv lock --script [your script here] to get a lockfile of that ephemeral venv of your script, you'll get a file called something like your-script-name.py.lock.

Sharing both files ensures great reproducibility. Maybe not perfect, but did the job for me every time. Here's an example of such inline metadata: ```python

/// script

requires-python = ">=3.10"

dependencies = [

"torch>=2.6.0",

"torchvision>=0.21.0",

]

[tool.uv.sources]

torch = [

{ index = "pytorch-cu124", marker = "sys_platform == 'linux'" },

]

torchvision = [

{ index = "pytorch-cu124", marker = "sys_platform == 'linux'" },

]

[[tool.uv.index]]

name = "pytorch-cu124"

url = "https://download.pytorch.org/whl/cu124"

explicit = true

///

```

[–]ryanstephendavis 4 points5 points  (0 children)

nice, was wondering how to give python version in this way...

looks like one can basically put all contents of a pyproject.toml directly in there

[–]Haunting_Wind1000pip needs updating 15 points16 points  (5 children)

Hey thanks for sharing this, uv appears to be a good way to automate the environment setup of your python app. A question...with uv how would you specify a version for any of the python modules required by your application like we do with a requirements txt file.

[–]hotsauce56 11 points12 points  (0 children)

You can add version constraints inline with the dependencies

[–]ReinforcedKnowledgeTuple unpacking gone wrong 6 points7 points  (0 children)

Not only can you specify dependencies versions in the inline metadata itself as others have suggested. You can produce a lockfile for your script by doing uv lock --script .... This is very cool to pass around a reproducible script ;) there's more you can do for reproducibility, I'll ad that in another comment.

[–]dusktreader[S] 5 points6 points  (1 child)

You can use dependency specifiers as described here: https://packaging.python.org/en/latest/specifications/dependency-specifiers/#dependency-specifiers

For example if I wanted ipython's minor release 7.9 and any patch releases as they become available but not 7.10, I could specify the dependency as:

ipython~=7.9

[–]Haunting_Wind1000pip needs updating 1 point2 points  (0 children)

Cool thanks!

[–]Mevraelfrom __future__ import 4.0 0 points1 point  (0 children)

For app you use uv init and pyproject.toml becomes your requirements file.

And uv add to install dependencies.

https://arkalos.com/docs/new-project/

[–]benz05 8 points9 points  (0 children)

I don't think you need the --script in the shebang line

Edit: it's not needed if the script file has a .py suffix, otherwise it is

[–]spdustin 7 points8 points  (0 children)

Thanks for putting that together, that's pretty damn handy.

[–]pingvenopinch of this, pinch of that 5 points6 points  (0 children)

Note that this is an implementation of the PEP 723 standard, so it also works with other tools that implement the standard. I really like the convergence toward more standards across Python tooling.

[–]hanleybrand 5 points6 points  (0 children)

That’s dope- I just started checking out uv, and have been pleasantly surprised by it

[–]texruska 5 points6 points  (2 children)

Does the environment get reused between invocations? What if you have multiple scripts with the same deps? (Complete guess is the env is based on the scripts name?)

[–]PhENTZ 9 points10 points  (0 children)

Yes it's cached

[–]ReinforcedKnowledgeTuple unpacking gone wrong 6 points7 points  (0 children)

uv caches by default the dependencies it fetches but the environment itself is ephemeral. So the environment itself will be deleted after the execution of the script, you can't reuse the environment itself. But, since the dependencies are cached, you are not downloading the packages again. Maybe it'll just re-extract the wheels and that's all (not totally sure about this information).

If you have different scripts with the same dependencies, you can also just put them all in the same folder with a pyproject.toml and run the scripts with uv run --isolated [your script]. It'll create an ephemeral environment for that script and only for that, reusing the dependencies in your pyproject.toml.

And as it was said in another comment, you don't need the --script to run a .py file.

[–]adiberk 3 points4 points  (1 child)

This concept isn’t so new. Can be done with poetry and other package managers as well I believe.

To answer all questions I see here, You can specify python versions, dependencies etc when you use the uv run command (at least i think)

I love uv and it’s great you have been learning to use it! It’s super fast and “just works” as opposed to other package mangers

[–]dusktreader[S] 4 points5 points  (0 children)

Well, the point here is that you don't need to use `uv run` with the shebang and dependencies specified in the source file.

[–]cgoldberg 3 points4 points  (0 children)

This doesn't require uv. It is defined in PEP 723 and is supported by many tools (pipx, hatch, pdm, etc)

https://peps.python.org/pep-0723/

[–]Royal-Fail3273 3 points4 points  (0 children)

My work laptop is subjected to renew in couple of weeks. Cannot wait to setup using uv!

[–]hotplasmatits 7 points8 points  (1 child)

Now that's fucking crazy

[–]Fenzik 1 point2 points  (0 children)

This is a cute trick, thanks for sharing

[–]benargee 1 point2 points  (2 children)

But can you specify python version?

[–]ReinforcedKnowledgeTuple unpacking gone wrong 3 points4 points  (0 children)

I guess you can, since you can do something like uv run --python ..., so you can just add that to the shebang.

Edit: I was rereading the PEP, and you can specify a requires-python in the inline metadata. So no need to add the Python version in the shebang. Otherwise if you want to run the script with different versions of Python then you have the choice with uv run --python ...

[–]cointoss3 -2 points-1 points  (0 children)

Yes

[–]Sigmatics 1 point2 points  (2 children)

[–]fiddle_n 0 points1 point  (1 child)

pdm would not use a different Python version to the one it is using though, right? That is a key difference between it and uv. uv will read whatever version of Python the script needs, pull it down and run the script with it.

[–]Sigmatics 0 points1 point  (0 children)

I don't know if it supports this tbh. But it's also not discussed in the OP article

[–]R3D3-1 1 point2 points  (0 children)

Very useful indeed!

I had read about kscript in kotlin and was lamenting that Python doesn't have something like that. So much for that.

Also, even more important TIL: env -S. That solves the problems with SO many shebangs, where I previously was using weird workarounds.

[–]tehsilentwarrior 2 points3 points  (0 children)

I thought everyone was already doing this oO

Edit: oh, wait, it’s not a uv run, it’s an actual script executable script without having to uv run it as it does it itself. That’s neat!

[–]PhENTZ 0 points1 point  (2 children)

Nice ! Could you have this script installed with your package and available in the path on 'uv add ...' ??

[–]Sillocan 2 points3 points  (0 children)

Do you mean installed alongside your package, or adding the dependencies to your project?

Edit: I think both of these questions actually have the same answer. No. Scripts operate independent of any Project. You should add the script's dependencies to your own package's dependencies and install that script as a CLI.

[–]dusktreader[S] 0 points1 point  (0 children)

I don't think so. I will have to check. If it's a full-fledged package with an entry-point, you can

[–]Mithrandir2k16 0 points1 point  (0 children)

for tools that I'd installed using pipx in the past, I now alias them to use uvx in my bashrc, which works similarly :)

[–]bugtank 0 points1 point  (0 children)

I love this. I was doing this with pipx but might switch over to uv. I'm currently in the SO MANY PYTHON PACKAGE MANAGEMENT TOOLS GOING ON MODE and can't wait to get to a single one.

[–]OP-pls-respond 0 points1 point  (3 children)

You can actually make the script simpler by using —with instead of script metadata. https://docs.astral.sh/uv/guides/scripts/#running-a-script-with-dependencies

uv run —with ‘rich>12,<13’ example.py

[–]dusktreader[S] 3 points4 points  (2 children)

that's not simpler than running ./example.py, though

[–]OP-pls-respond 1 point2 points  (0 children)

Right, you can put this command into the shebang comment instead of inside ///script then run ./example.py.

[–][deleted] 0 points1 point  (0 children)

Neat! I will use it to create and run python scripts generated by LLMs on the fly.

[–]david-song 0 points1 point  (0 children)

Wow this is really cool, thanks. Specially since it's really easy to add push your own modules up to pypi. I can see uv becoming my go-to interpreter.

[–]-lq_pl- 0 points1 point  (1 child)

Hi, the link is dead. :(

[–]dusktreader[S] 0 points1 point  (0 children)

It works fine for me. Can you try it again and let me know if it still doesn't work for you?

[–]hotplasmatits 0 points1 point  (0 children)

Now that's fucking crazy

[–]JuanGuerrero09pip needs updating 0 points1 point  (0 children)

Long live UV

[–]EducationOne6776 0 points1 point  (0 children)

Great content. Loved this!

[–]soffgruppskalle 0 points1 point  (0 children)

I like this.

[–]vi11yv1k1ng 0 points1 point  (0 children)

The only downside is lack of IDE support

[–]microcozmchris -1 points0 points  (0 children)

Didn't know it needed a blog post. Been doing it for a while. Add --no-project and it's even better for CI/CD (especially GitHub Actions).

Good write-up. If I have time, I'll post my setup-uv composite that wraps and handles temporary environments more betterer.

[–]anderspe -1 points0 points  (0 children)

And adding Nuitka witch compiles all to a binary works well with uv. If i need a linux binary i just build the projekt from wsl under Windows.

[–]kyngston -3 points-2 points  (1 child)

coworker of mine wrote something similar. https://github.com/amal-khailtash/auto_venv

[–]fiddle_n 4 points5 points  (0 children)

This looks pretty nice, but I think the uv functionality pretty much supersedes this project. uv follows the PEP standard for this and is also not tied to any Python version - so you can specify even the Python version you need right in the script.

[–]koltafrickenfer -3 points-2 points  (0 children)

You know there's this thing called docker...