Testing multiple versions of Python in parallel
Daniel Roy Greenfeld wrote about how to test your code for multiple versions of Python using `uv`. I follow up with a small improvement to the Makefile. I was reading my RSS feed when I came across Daniel Roy Greenfeld’s article about using uv to run unit tests on multiple versions of Python. Daniel writes: But what if I want to test a particular version of Python? Then I simple specify the version of Python to run the test: Here’s where it gets fun. I can use a This is fantastic! I’ve used However, there’s one disadvantage to the technique Daniel describes above: every time uv switches between Python versions, it will have to remove and replace the whole For example, this is what happens when I consecutively run the tests on a project with two different python versions: This most likely isn’t much of a problem, because of how fast Caveat emptor: This presumes that your tests don’t have any real side-effects, and that they can be run fully simultaneously without clashing with one another (shared databases like sqlite, or creating test files could wreak havoc here). We need to deploy a few tricks so that Okay, there’s a lot to unpack here. Let’s dive in. The Similarly easy to understand, the The The final target, But what does it do? Well, for starters, if you use it exactly like Daniel’s version, it behaves exactly the same way, but it doesn’t replace .venvs all the time: But more interestingly, we can now run run all tests in parallel across versions of Python! For real-world use, I would most likely also add a
Makefile
(or a justfile) to test on multiple Python versions.: ## Run all the tests for all the supported Python versions
uv
for only a couple of projects, and already I cannot imagine ever giving it up again..venv
directory and reinstall all dependencies.
uv
is, but it’s still not ideal. In addition, using Makefiles to run multiple commands sequentially is sooOOoOo boring, especially now that we have computers with insane processor counts. Let’s run these commands in parallel!uv
won’t stumble over itself and try to re-use the .venv
directory. In the end, this ends up being slightly less elegant than I wanted, but I guess that’s a lesson for me in believing slightly too much in Makefiles.: :
PYTHON_VERSIONS :=
TEST_TARGETS :=
:
:
:
.PHONY
and .PRECIOUS
lines are easily explained: there will never be a file called testall
, so no need to check if it exists or not, and don’t delete the intermediate files created when we initiate a venv.PYTHON_VERSIONS
just contains all the versions of Python we want to test against. And the TEST_TARGETS
represents the actual targets in the file we want to execute for (tests-3.13
, tests-3.12
, etc).pyvenv.cfg
target just says “if anyone needs .venv-whatever/pyvenv.cfg
, here’s how to create it”. It very basically calls uv venv
and initialises a virtualenv with the correct Python version. The pyvenv.cfg
file is created by uv
when it sets up the virtualenv, satisfying the Make rule..tests-%
, we create manually (touch $@
) after running uv run
inside the proper virtualenv. This is most likely the part I’m the least happy with; I wish uv
had a way of either prefixing venv dirs, or the ability to configure it to say “Python version 3.12 uses .venv-3.12 as the venv dir” or something.
venv=.venv-3.10 && && \
=============
venv=.venv-3.11 && && \
=============
venv=.venv-3.12 && && \
=============
venv=.venv-3.13 && \ && \
=============
clean
target, and I would make the test targets depend on the actual pyproject.toml
and .py
files, but that’s an exercise left for the reader.