Charm Tech's positive experience with uv+tox-uv

What is it?

The uv tool from astral.sh and the tox-uv plugin for tox.

What’s great about it?

uv is gaining popularity for Python projects because it’s modern, ergonomic and fast.

You can use uv to create a single lock file for all of your project’s runtime and development dependencies. uv manages this lock file according to the dependencies and dependency groups that you specify in pyproject.toml.

The great thing about tox-uv is that it makes tox aware of the way you’ve arranged your project’s dependencies.

This lets you set up separate dependency groups for static analysis, unit tests, integration tests, and even docs, then reference these groups from the corresponding tox environments. So that, for example, running tox -e unit installs the locked dependencies from your dev dependency group before running pytest.

Now there’s a single source of truth for all your deps: declaration in pyproject.yaml and pins in uv.lock.

Quick start: use tox with tox-uv

sudo snap install astral-uv --classic
uv tool install tox --with tox-uv
uv tool update-shell
# restart shell if needed
tox

Make sure that tox in the path is the one installed via uv tool (or in any case it’s installed with the required plugin) and not your older tox. You can run tox --version to make sure.

Use uv with Charmcraft

We’re working with the Charmcraft team to switch the kubernetes and machine profiles of charmcraft init to use tox-uv, starting from Charmcraft 4.

Even better, Charmcraft already supports uv as a build system when packing charms. To use uv, specify the uv plugin in charmcraft.yaml, then run charmcraft pack as before:

parts:
    charm:
        plugin: uv
        source: .
        build-snaps:
            - astral-uv

When Charmcraft packs the charm, uv figures out the charm’s dependencies using the same lock file as tox. This setup should also be coming to the Kubernetes and machine profiles in Charmcraft 4!

Simple example: Dima’s toy charm:

Complex example: canonical/operator, the Ops library:

Caveats

The tox-uv plugin dropped support for Python 3.8 before the dependency-groups feature has landed. This means that if you still target Ubuntu 20.04 and run your unit tests on Python 3.8, you need to do some extra work:

  • ensure that tox run on modern Python (e.g. system, or the latest, 3.13)
  • use Python 3.8 for the commands that tox invokes

Here’s an example of doing just that in our repo, which allows running tox -e py3.8-unit

Other options

We’re still experimenting with both make and just in some of our repos. We’re not set on one tool to rule them all, but we wanted to report good experience with uv+tox-uv specifically.

References

Your Charm Tech engineers, @dimaqq and @davidwilding.

6 Likes

Love this post. ty!

I’ve been using uv as well with charms and generally enjoy it. I haven’t used tox-uv yet but I’m looking forward to trying.

Something I found convenient was, rather than installing tox, just using uvx tox ... in all my invocations. Its a minor thing, but fun that I don’t need tox installed for every dev environment. Have you avoided that for any reason, or just preference/didn’t think of it?

Since switching, I feel like I haven’t got the right pattern connecting my IDE to my uv-driven dependencies. Do you have a pattern for this? Before uv I’d do:

  • python -m venv venv; source venv/bin/activate; python -m pip install -r requirements
  • maybe install some other test dependencies from tox.ini’s environments
  • use that venv in the IDE

Right now, I’m doing something like:

  • uv venv .venv; source .venv/bin/activate; uv pip install -r pyproject.toml --all-extras
  • use that venv in the IDE

Is that your process too? It seems to work, but feels like I’m holding uv wrong

1 Like

In the Telco and TLS team, we have also been using uv and tox-uv for a while with good success. In general, my personal workflow on a project looks like this:

  • git clone my-project; cd my-project
  • uv sync --all-groups; source .venv/bin/activate
  • export PYTHONPATH=$(pwd)/lib # for the LSP in nvim to find the charm libs

@ca-scribner, compared to the workflow you posted, I let uv automatically create the venv, and we use dependency groups instead of extras. I think they make more sense for the things we use them for (testing tools, linters, etc.), as extras are meant to let users install extra functionality.

1 Like

Ah nice, ty! I didn’t realize uv sync actually created the venv. That feels much nicer

Also, I didn’t realize there’s both dependency-groups and optional-dependencies. For anyone else reading, here is a nice comparison. Essentially:

  • dependency-groups: for things like test dependencies that will not ship with your code
  • optional-dependencies: for things that will ship with your code as optional additional installs (eg: mypkg[extra-thing])

In observability, we’ve been using the wrong one! This explains why our uv.lock files have some bloat, too.

2 Likes