Chris Amico

Journalist & programmer

Migrating a small Python library to uv

A lot of my old Python projects are sitting in limbo these days, because I started them using an old packaging system that I’ve since abandoned. There was the pip-tools phase, the pipenv era, a brief interlude with poetry. Some just used virtualenv (or virtualenvwrapper or python -m venv).

I would like to move all of these onto uv, because I think it does everything these other tools do, and does all of it faster and in only one tool.

Python’s biggest shortcoming has always been setting up a development environment, but uv really does solve a lot of these problems.

I wanted to fix a bug on feed-to-sqlite yesterday, so I decided it migrate it to uv first. Here’s what I did and where I ran into obstacles.

There is a tool called migrate-to-uv that seemed like it would do this for me in one step. It covers most package managers, but I wasn’t actually using one of those. I used an old fashioned setup.py file and setuptools, because Python’s packaging ecosystem has largely split into tools for libraries and tools for projects. So I had to do this by hand.

The newer standard, which seems to unify libraries and projects, is pyproject.toml. (But setup.py still works, sort of, if you’re using setuptools.)

Running uv init --lib generated a stub pyproject.toml file. I tried a couple versions of init; using the --lib skipped making a main.py file that I’d have to delete and picked up some of the existing project metadata. From there, it was a lot of copy/paste from setup.py to pyproject.toml. Nothing hard, but it would be nice to automate this. Bits of metadata go in unexpected places, but it’s fine and the documentation is good.

Since pyproject.toml is agnostic on how you build and publish, you have to tell it what packaging system you’re using. This is what uv needs (as of yesterday, at least; check the docs whenever you do this because everything changes):

[build-system]
requires = ["uv_build>=0.8.13,<0.9.0"]
build-backend = "uv_build"

Except that broke things and the library wouldn’t build or install once that was in. It turns out the uv maintainers think Python libraries should live in a src folder, not in the root directory, and so it kept looking for src/feed_to_sqlite/__init__.py, which doesn’t exist.

The fix is to tell uv to stop looking there, like this:

[tool.uv.build-backend]
module-root = ""

Now the library builds. Can I run tests?

My old setup had an optional test dependency, so I could use pip install feed-to-sqlite[test] to get the library and pytest installed. In pyproject.toml, that gets added like this: uv install --group test pytest, and then installed by running uv sync --group test or uv sync --all-groups. I don’t know if there’s a less verbose version. Mostly this happens in Github Actions, so it doesn’t matter.

The last step was publishing this new version of the library.

Most of this was updating my publish.yml workflow. I’m also slowly switching my libraries over to use PyPI’s Trusted Publisher system, which negates the need to manage credentials.

The one hiccup there was that I hit a permissions error a few times. I finally traced it to a missing permissions block in the workflow. Adding this block to the deploy job fixed it:

permissions:
  id-token: write

I don’t fully understand what that does. The only reference to it I found was in the actions/deploy-pages workflow. (Shoutout to Simon Willison for opening an issue to ask about it.) But that solved the problem.

Since I got this migrated, I’ve closed a couple longstanding issues, and I have more fixes planned. This is all going toward another side project that I haven’t actually started yet.