tox vs nox: Python Test Automation Compared
tox vs nox compared: TOML or Python configuration, testing across Python versions, environment reuse, packaging behaviour, uv support and which to pick.
Use nox if you want to write your automation in plain Python, and tox if you prefer declarative configuration. Both create isolated environments, install your project into them and run your test, lint and docs commands, usually across several Python versions.
At a glance
| tox | nox | |
|---|---|---|
| Configuration | tox.toml or [tool.tox] in pyproject.toml (INI is deprecated) | noxfile.py, which is Python |
| Unit of work | Environment | Session |
| List what is defined | tox list | nox -l |
| Run one | tox -e 3.13 | nox -s tests |
| Environments between runs | Reused; recreated automatically if the config changes, or with -r | Deleted and recreated every run by default; -r reuses them |
| How it installs your project | Builds an sdist by default; package can be wheel, editable or skip | Whatever your session does, such as session.install(".") |
| uv | Third-party tox-uv plugin | Built-in uv backend |
The behaviour in the table was checked with tox 4.67.9 and nox 2026.10.20 on Python 3.12 and 3.13.
What problem do they solve?
Running pytest in your own environment tests whatever happens to be installed there. A dependency you forgot to declare, or a file left out of your built package, passes locally and fails for users. tox’s documentation lists its first job as checking that your package builds and installs correctly under different environments, then running your tests in each of them.
tox
tox’s documentation says its INI format is frozen and deprecated and recommends TOML. Here is a tox.toml that tests on two Python versions and lints:
env_list = ["3.12", "3.13", "lint"]
[env_run_base]
deps = ["pytest"]
commands = [["pytest"]]
[env.lint]
deps = ["ruff"]
commands = [["ruff", "check", "."]]
package = "skip"
[env_run_base] applies to every environment, and [env.lint] overrides what it needs. Each command is a list of arguments. Running tox creates .tox/3.12, .tox/3.13 and .tox/lint; tox list shows them; and tox -e 3.13 runs one. The same configuration in the older INI format still works:
[tox]
env_list = py312, py313, lint
[testenv]
deps = pytest
commands = pytest
[testenv:lint]
deps = ruff
commands = ruff check .
Packaging. tox’s default is to build a source distribution and install it into each environment, which is why the lint environment above sets package = "skip": without it, the lint environment also builds and installs the project. Other modes are wheel (documented as much faster to install), sdist-wheel, editable and external.
Reuse. tox creates an environment on the first run and reuses it afterwards. In the test project the first run took about 23 seconds and a second run about 4. It recreates an environment automatically when something such as the Python version or the dependencies changes, and tox -r forces it.
nox
import nox
@nox.session(python=["3.12", "3.13"])
def tests(session):
session.install(".", "pytest")
session.run("pytest")
@nox.session
def lint(session):
session.install("ruff")
session.run("ruff", "check", ".")
nox -l lists tests-3.12, tests-3.13 and lint. nox -s tests runs both versions, and sessions are ordinary functions, so you can loop, branch, read files or share helpers. By default nox deletes and recreates each virtual environment every time it runs; the documentation says pip’s caching keeps the reinstall quick, and nox -r or reuse_venv=True on a session reuses environments. session.install(".") is a regular install of the project, not an editable one.
Using uv
With nox, set the backend in the noxfile and have uv installed (the nox[uv] extra installs it):
nox.options.default_venv_backend = "uv"
The output then reads Creating virtual environment (uv) and uv pip install . pytest. A fallback syntax, uv|virtualenv, uses uv when it is present and virtualenv otherwise. With the uv backend nox does not install pip into the environment, so install it with session.install("pip") if a session needs it.
With tox, install the tox-uv plugin next to it (pip install tox tox-uv). The tox documentation describes it as replacing both environment creation and package installation with uv, and the run log shows uv venv and uv pip install commands in place of virtualenv and pip. See uv vs pip for how uv’s installer differs.
Catching packaging mistakes
Because tox installs the built package, it catches the mistake described in src layout vs flat layout, where a subpackage is left out of the wheel. On the test project, tox failed with ModuleNotFoundError: No module named 'mypkg.sub' in both a src and a flat layout. It does so because the tests run with pytest inside the environment, against the installed package. Starting them with python -m pytest in a flat layout puts the working directory on the import path, which hides the problem.
Which should you pick?
- A list of environments and commands: tox, with
tox.toml. - Conditions, loops, generated sessions or shared helpers: nox.
- uv as the installer, with no extra plugin: nox. With tox, add
tox-uv. - Already comfortable with one: stay. The core idea is identical.
Alongside CI
tox’s documentation describes it as a frontend to CI servers that cuts boilerplate. Let CI handle platforms, caching and secrets, and call tox or nox from it, so a failing job reproduces locally with one command. For choosing test tools in the first place, see the best Python testing tools compared, and for the tests themselves, pytest fixtures explained.
Related
Upgrading Python versions is where a test matrix pays off; see Python 3.13 end of life and Python 3.14 vs 3.15. For managing several interpreters locally, see pyenv vs uv. The pytest resource page covers the runner both call.
Frequently Asked Questions
What is the difference between tox and nox?
Both create isolated virtual environments, install your project and run tests and other checks, often across several Python versions. tox is configured declaratively, in tox.toml or a [tool.tox] table in pyproject.toml, and its older INI format is deprecated. nox is configured in a Python file called noxfile.py, so you can use ordinary Python logic.
Should I use tox or nox?
Use tox if you like declarative configuration and a list of environments, and nox if you want to write sessions as Python functions with loops, conditions and shared helpers. The core idea is the same, so staying with the one you already know is reasonable.
Do tox and nox work with uv?
nox has uv as a built-in virtual environment backend: set nox.options.default_venv_backend to uv, with uv installed. tox gets uv support from the separate tox-uv plugin, which replaces both environment creation and package installation with uv.
Do I need tox or nox if I use GitHub Actions?
They work together. tox’s documentation describes it as a frontend to continuous integration servers that cuts boilerplate, so CI calls tox or nox and developers run the same command locally.
Can I just use pytest instead of tox or nox?
pytest runs tests in whatever environment you are in. tox and nox create separate environments, install your project into them, and can run several different checks, such as tests, linting and docs, from one entry point. By default tox installs a built sdist, so a file missing from the package fails the tests.