Skip to content

Do you still need tox or nox if you use uv?

For many projects, no. uv can run your test suite against multiple Python versions without any extra tool. But tox and nox do more than version switching, and the gap between “run pytest on 3.12” and “test five Python versions against two dependency sets in parallel” is where they still matter.

What uv handles on its own

uv run --python selects a Python version and executes a command in your project’s environment. If that version is not installed, uv downloads it automatically:

uv run --python 3.12 pytest

Wrap this in a loop to test multiple versions:

for version in 3.11 3.12 3.13 3.14; do
    echo "=== Python $version ==="
    uv run --python $version pytest
done

A justfile recipe makes this repeatable:

test-all:
    #!/usr/bin/env bash
    for version in 3.11 3.12 3.13 3.14; do
        echo "=== Python $version ==="
        uv run --python $version pytest || exit 1
    done

If your project already uses a Makefile, a pattern rule covers the same ground and pairs with UV_PROJECT_ENVIRONMENT to keep a separate virtual environment per Python version:

test-py%:
	UV_PROJECT_ENVIRONMENT=.venv-$* uv run --python $* pytest

test-all: test-py3.10 test-py3.11 test-py3.12 test-py3.13

make -j4 test-all runs the matrix in parallel. See How to replace tox with uv and a Makefile for the complete walkthrough, including the dependency-variant matrix and the gotchas it exposes.

All three forms (shell loop, justfile, Makefile) cover the most common multi-version testing scenario: same dependencies, same test command, different Python interpreter. For an application or library with a single dependency set and a flat test suite, any of them is enough.

Where tox and nox add value

Both tools are test orchestrators. They manage environments, dependencies, and commands as a unit. The features that justify adding one to your stack:

Dependency matrices

Testing against multiple dependency versions (not just Python versions) is the strongest case for tox or nox. A library that supports SQLAlchemy 1.4 and 2.0 needs to test both. tox expresses this declaratively:

tox.ini
[tox]
env_list = py{312,313}-sqlalchemy{14,20}

[testenv]
deps =
    sqlalchemy14: sqlalchemy>=1.4,<2
    sqlalchemy20: sqlalchemy>=2.0,<3
commands = pytest {posargs:tests}

nox expresses the same thing in Python:

noxfile.py
import nox

@nox.session(python=["3.12", "3.13"])
@nox.parametrize("sqlalchemy", ["1.4", "2.0"])
def tests(session, sqlalchemy):
    session.install(f"sqlalchemy~={sqlalchemy}.0", "pytest")
    session.run("pytest")

Reproducing this with a shell loop means hand-managing virtual environments, pinning dependency versions per iteration, and cleaning up afterward. Possible, but error-prone enough that a dedicated tool pays for itself.

Parallel execution

tox runs all environments concurrently with tox p. nox supports parallel execution through external tools or its own --sessions flag. Running four Python versions in parallel cuts CI wall-clock time proportionally. A shell loop runs them sequentially; a Makefile with per-version targets can parallelize with make -j, though it lacks tox’s logging-per-environment and result aggregation.

Multiple commands per environment

Test orchestrators group related commands: install dependencies, run linters, run tests, generate coverage, upload reports. Each group runs in its own isolated environment. Encoding these steps in tox or nox means the CI configuration stays thin (just tox or nox), while the test logic lives in the project and works identically on any developer’s machine.

Environment caching

tox and nox cache virtual environments between runs and only recreate them when dependencies change. A shell loop with uv run --python rebuilds the project environment on every Python version switch because .venv can only hold one interpreter at a time. Setting UV_PROJECT_ENVIRONMENT=.venv-X.Y per version keeps each one on disk, which closes most of the gap for local development; tox and nox still win when dependency trees are large enough that the install itself is the expensive step.

Choosing between tox and nox

Both tools solve the same problem. The main difference is configuration format:

  • tox uses INI-based configuration (tox.ini). The format is concise for standard matrices but becomes awkward when logic enters the picture (conditional dependencies, platform-specific commands, dynamic environment names).
  • nox uses Python (noxfile.py). Any logic you can write in Python, you can use in your test sessions. This flexibility costs a steeper learning curve.

For a project with a static test matrix (a few Python versions, maybe two dependency variants), tox is the simpler choice. For anything that requires conditional logic or dynamic configuration, nox handles it more cleanly.

Both tools integrate with uv

Neither tox nor nox requires giving up uv’s speed. The tox-uv plugin replaces pip and virtualenv with uv inside tox environments, and nox has a built-in uv backend that does the same. In both cases, uv handles the slow parts (environment creation and dependency installation) while the orchestrator handles the coordination.

Install tox with uv backing:

uv tool install tox --with tox-uv

Enable uv in nox:

noxfile.py
@nox.session(python=["3.12", "3.13"], venv_backend="uv")
def tests(session):
    session.install("pytest")
    session.run("pytest")

Decision guide

Use uv run --python with a shell loop, justfile, or pattern-rule Makefile when:

  • You test against multiple Python versions with a single dependency set
  • Your test command is one line (pytest, pytest --cov)
  • You do not need dependency variant testing
  • You already use one of those runners for the project’s other commands (lint, format, build)

Add tox or nox when:

  • You test against combinations of Python versions and dependency versions
  • You run multiple distinct commands per test environment (lint, test, coverage, docs)
  • You want parallel environment execution
  • You maintain a library with a wide support matrix
  • You need environment caching for large dependency trees

Tip

Starting with uv run --python and upgrading to tox or nox when the need arises is a reasonable path. Both tools layer on top of uv rather than replacing it.

Learn More

Last updated on

Please submit corrections and feedback...