cat posts/2025-10-28-uv-and-python-packaging.md

uv, and the end of my Python packaging pain

· 3 min read · python tools packaging

The first line of my expenses script, the one I wrote last year, is now this:

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

And just below it there’s a comment block that lists its dependencies:

# /// script
# requires-python = ">=3.12"
# dependencies = ["openai", "pillow", "pdf2image"]
# ///

That’s the whole environment. No virtualenv to create, no requirements.txt, no remembering which Python it was written against. I make the file executable, run it, and uv reads that block, makes a cached environment with those packages and the right Python, and runs the script in it. The first run on a new machine takes a few seconds. After that it starts as fast as plain python. The comment format is a standard, PEP 723, so it isn’t even uv-specific, though uv is what made me actually use it.

Emily Hunt’s essay, uv is the best thing to happen to the Python ecosystem in a decade, has been going round this week, and it’s a good, plain walkthrough of the project side of uv. If you have a Python codebase and haven’t tried it, read that. I’m mostly going to talk about my end of it, which is small scripts and tools on three operating systems.

I’ve never been a Python person, really. I’m a C# person who writes Python when it’s the obvious choice, which is a lot of the time for scripts, and I’ve lost more hours to Python environments than to the actual Python. Over the years I’ve had virtualenv, virtualenvwrapper, pyenv, pipx, and at one point a Homebrew Python, a system Python, and a pyenv Python on the same Mac, each with a different pip, and a script that worked or didn’t depending on which terminal I’d opened. On Windows it was worse, because the Python launcher has its own opinions.

uv replaces all of those for me. It’s one binary, written in Rust by Astral, the people who make the ruff linter. It installs Python itself (uv python install 3.14 got me 3.14 on the day it came out, earlier this month), it makes and manages virtual environments, it installs packages a lot faster than pip, and it installs command-line tools in their own isolated environments, the job I used pipx for. On my machines, setting up the environment for a project at work with about sixty dependencies went from a bit under a minute with pip to about three seconds with a warm cache. I haven’t benchmarked it properly and I don’t need to, the difference is not subtle.

The setup is the same on the Macs, the Windows laptop and the Linux box, which is the thing I care about most, since that’s how I like everything to work. uv is installed by the install script on Mac and Linux, and by winget on Windows, and the chezmoi config makes sure the tools I want are there:

uv tool install ruff
uv tool install llm

For a one-off I don’t want to install at all, uvx runs a tool in a throwaway environment, like npx. I use it a few times a week.

One wrinkle on Windows. The #!/usr/bin/env -S line means nothing there, so on the laptop I run those scripts as uv run --script expenses.py. It reads the same block and works the same way. I have a PowerShell function that does it for any .py file in my scripts folder, so I don’t have to think about it.

My reservation is the one people raised when uv came out. It’s made by a venture-funded company, and at some point Astral will need to make money, and I don’t know yet how they plan to. Everything is open source under MIT and Apache licenses, so if it went badly the community could fork it, and the standards it uses (PEP 723 for scripts, pyproject.toml for projects) aren’t Astral’s. That’s a lot better than most of the tools I’ve depended on. It’s still a dependency on one company for the thing that sits underneath all my Python, and after the Terraform license change I don’t take that kind of thing as lightly as I used to.

For now, though, it’s the first time in nearly twenty years of on-and-off Python that I haven’t had to think about environments at all, and I don’t intend to start again.