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


For many projects, no. [uv](https://pydevtools.com/handbook/reference/uv.md) can run your test suite against multiple Python versions without any extra tool. But [tox](https://pydevtools.com/handbook/reference/tox.md) and [nox](https://pydevtools.com/handbook/reference/nox.md) 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:

```bash
uv run --python 3.12 pytest
```

Wrap this in a loop to test multiple versions:

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

A [justfile](https://just.systems) recipe makes this repeatable:

```just
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:

```makefile
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](https://pydevtools.com/handbook/how-to/how-to-replace-tox-with-uv-and-a-makefile.md) 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:

```ini {filename="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:

```python {filename="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](https://pydevtools.com/handbook/reference/tox-uv.md) plugin replaces pip and virtualenv with uv inside tox environments, and nox has a [built-in uv backend](https://nox.thea.codes/en/stable/config.html#configuring-a-session-s-virtualenv) 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:

```bash
uv tool install tox --with tox-uv
```

Enable uv in nox:

```python {filename="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

- [How to replace tox with uv and a Makefile](https://pydevtools.com/handbook/how-to/how-to-replace-tox-with-uv-and-a-makefile.md)
- [How to test against multiple Python versions using uv](https://pydevtools.com/handbook/how-to/how-to-test-against-multiple-python-versions-using-uv.md)
- [How to use uv to speed up tox](https://pydevtools.com/handbook/how-to/how-to-use-uv-to-speed-up-tox.md)
- [tox-uv reference page](https://pydevtools.com/handbook/reference/tox-uv.md)
- [tox documentation](https://tox.wiki/)
- [nox documentation](https://nox.thea.codes/)
- [nox uv backend](https://nox.thea.codes/en/stable/config.html#configuring-a-session-s-virtualenv)
