UV Usage: run, add, remove

I’ve been moving all my python code over to be managed by uv in the last few months. With the release of 0.3.x, I’ve gone from just playing around with it to fully buying into it as my python package manager.

The new release added a whole host of new sub-commands, initially marked as experimental, but the label dropped within days. Switching over to use them also meant changing how I was using uv, which may mean I was using it the wrong way to begin with.

Below, I’m describing what I’ve been doing in the hopes it helps make life easier for anyone giving it a whirl.

Prior usage

Before the new release, I was basically using uv as a sort of pip and venv replacement. I’d use it to create my virtual environment and then pip install any dependencies into it. On my system it was so much faster that using it meant it basically got out of the way of actual development. It’s minor, but that feeling of instant response felt magical.

Those commands, uv pip and uv venv still work, but with the new workflow they feel almost redundant. I suppose for managing existing projects they still have a place, since you can use uv immediately without having to change anything from a project that was set up using traditional pip and venv.

New workflow

Switching to the new commands meant changing the way I looked at a project. I’ll explain by working backwards, which I think will also help explain why anyone would want to bother doing it at all.

uv run

In a project managed by uv, the run command is the workhorse. It allows running any tool or part of the program with the environment fully set up as needed. For example, with zpdatafetch I have a binary named zpdata which acts as a wrapper to provide access to all the library functions from the command-line. This helps both for ad-hoc data checks and for checking that the library still does what is expected while in development.

With uv run there is no need to pip install the package or alternatively, update the PYTHONPATH to include the local directory. Instead, I can simply use uv run zpdata ... and uv takes care of managing the library path and even the binary search path to make sure I pick up the local copy.

It works just as well for wrapping pytest so that running tests quickly is simple from the start of the project. Previously, I’d have used make or doit to make sure I had a quick command that did this, but now it’s just there ‘for free’ with uv.

uv add and uv remove

Speaking of pytest - uv also changes the approach to managing dependencies of the project. In doing so, it finally supports the split of prod and dev dependencies as well. Instead of pip install or uv pip install the new workflow expects you to uv add <package>. This still uses uv’s hyper-fast magic to set the package up in your environment. For packages like pytest adding the --dev flag will keep it separate in the dev dependencies.

For zpdatafetch this means initial setup is now:

uv add beautifulsoup4 httpx lxml keychain
uv add --dev pytest ruff setuptools build wheel

Of course, that’s not needed in practice because all those dependencies are now captured in the pyproject.toml file. So unless something’s changing, there’s no need to run them. On initial git clone of the repository, it’s possible to install them with uv sync or even just ignore that and go straight to uv run which will do the same thing before the first script is run to ensure the environment is consistent.

Next post I update on how I migrated zpdatafetch to uv including updating the github actions to use uv for build and test before publishing to pypi.

© Doug Morris
Written on 27 August 2024