Recently I've been updating some of my libraries to Python 3.10+ after Python 3.9 has finally reached end of life.
The upgrade to Python 3.10 is a relatively simple one, but I thought it be a good idea to run tests on different versions of Python.
Existing Tooling
tox is a common tool for this use case, it allows you to define environments declaratively.
requires = ["tox>=4"]
env_list = ["3.14", "3.13", "3.12", "3.11", "3.10"]
[env_run_base]
description = "run unit tests"
deps = [
"pytest>=8",
"pytest-sugar"
]
commands = [["pytest", { replace = "posargs", default = ["tests"], extend = true }]]
Similarly nox let's you do the same but in a more imperative way.
uv to rule them all
The thing is, tox and nox requires learning and using a new set of tools, whilst that's completely fine uv has spoiled us with the convenience of using a single tool.
There's already a lot that can be replaced by uv, for example:
python -m venv
uv venv
pip install ...
uv pip install ...
pyenv install ...
uv python install ...
I read an article from Simon Willison where he already worked out how to test a package with different Python versions with the -p flag.
uv run -p 3.10 pytest
This is pretty much job done here! But we can go a bit further ...
Extras
You can specify any extras and dependency groups via --extra and --group respectively:
uv run -p 3.10 --extra cli pytest
uv run -p 3.10 --group test pytest
Overriding Package Versions
A common use of tox is to test over different package versions, for example, if a package needs to support both pydantic==1.*.* and pydantic==2.*.*. We can also do this in uv with the --with or -w options:
uv run -w 'pydantic==1.*.*' pytest
uv run -w 'pydantic==2.*.*' pytest
This also works for a set of deps in a requirements.txt file:
uv run --with-requirements requirements-production.txt pytest
Simon's article also mentions using --with-editable to test with the local version of the code, which can come in handy:
uv run --with 'pydantic==1.*.*' --with-editable 'mylocal-package' pytest
Using an isolated venv
Simon also mentions using an isolated venv:
uv run --with 'pydantic==1.*.*' --isolated pytest
to avoid your test venv contaminating your development venv or vice versa.
I found this most useful when I needed to run tests in parallel, something like:
parallel "uv run -p {} --isolated pytest" ::: 3.{10..14}
This is a quick proof of concept but you can probably write a more robust bash or python script.
Conclusion
tox still remains useful especially with something like tox-uv. But if you're using uv anyway it may be useful to learn all the options mentioned above.
I've already had a lot of success using uv to quickly debug issues caused by version incompatibilities in pandas, something that just takes a bit more time to setup with tools like tox.