tox review
tox creates named Python environments, installs the project and test dependencies into them, and runs configured commands. One file can cover interpreter versions, dependency factors, package builds, pytest, type checking, linting, documentation, and CI labels. TOML is now the preferred configuration; INI still loads but is deprecated and frozen. Version 4.60.0 added {home} and {tox_root_name} substitutions plus earlier provisioning, while current 4.60.1 replaces raw tracebacks with handled errors for malformed configs, invalid skip regexes, unreadable files, and empty TOML commands.
Our tox 4.60.0 install took 0.5 seconds, used 10 MB across 13 packages, imported in 0.66 seconds, and had no known audit findings; PyPI now serves the unmeasured 4.60.1 patch. Use tox for a real package matrix, and keep one-environment applications on a direct test command unless isolation proves useful.
We installed it
| Install | ✓ · 0.5s | 13 packages on disk · 10 MB |
| Import | ✓ | import tox in 0.66s · pure Python · py.typed · requires Python >=3.10 |
| Known vulns | 0 | (pip-audit) |
Answers from our run
Does tox install cleanly?
Yes. In a fresh container with an empty cache, pip install tox finished in 0.5s, leaving 13 packages and 10 MB on disk. pip-audit reported no known vulnerabilities.
What does tox need to run?
Python >=3.10, and nothing compiled: it is pure Python. In our run import tox succeeded in 0.66s, and the package ships py.typed for type checkers.
tox or nox: which should you use?
nox: Use it when Python functions express sessions and matrix logic more clearly than declarative TOML. Our tox 4.60.0 install took 0.5 seconds, used 10 MB across 13 packages, imported in 0.66 seconds, and had no known audit findings; PyPI now serves the unmeasured 4.60.1 patch.
When should you not use tox?
The service deploys one locked Python environment and has no compatibility matrix; a direct pytest command has less orchestration state
Use it if
- A Python distribution must be built and tested under several interpreter or dependency versions
- Local developers and CI should choose the same test, lint, type-check, and documentation environments by name
- Packaging mistakes need to surface by installing an sdist or wheel before tests import the project
- A matrix needs factor-specific dependencies, labels, bounded parallel execution, or ordered coverage aggregation
- The service deploys one locked Python environment and has no compatibility matrix; a direct pytest command has less orchestration state
- You expect the tool to download missing interpreters; tox discovers Python versions already installed on the machine
- Fast locking and interpreter installation are the main jobs; uv provides those directly while tox delegates environment creation and package installation
- The project already uses Hatch environments for testing and release work; a second environment manager duplicates config and caches
- You require future features without leaving tox.ini; INI is documented as deprecated and frozen, and new configuration work targets TOML
Setup reality
We installed tox 4.60.0 in a fresh Python 3.12 Bookworm sandbox in 0.5 seconds. The install left 13 packages occupying 10 MB. The measured release declares 16 direct dependencies, requires Python 3.10 or newer, and is pure Python. It ships py.typed and uses the MIT License. import tox completed in 0.66 seconds, and pip-audit reported zero known vulnerabilities. PyPI now serves 4.60.1; we did not rerun the lab install for that patch.
Place tox.toml, a [tool.tox] section, or the deprecated tox.ini at the repository root. Every requested interpreter must already be discoverable. Tox keeps environments, logs, and package builds under .tox. Test environments build an sdist by default. Set package = "wheel" to test a wheel, use sdist-wheel to make that wheel from the sdist, or skip package installation for a lint-only job. Version 4.60.1 improves configuration failures but does not remove the need to migrate INI.
The first run of each matrix creates virtual environments and installs their dependencies, so the 0.66-second import says nothing about full project runtime. Later commands reuse cached environments. Added requirements are installed, while removals can trigger recreation; tox run -r forces a clean environment when state is doubtful. Host variables are filtered. Name required entries in pass_env, set controlled values in set_env, and allow external programs narrowly through allowlist_externals instead of broad secret or command wildcards.
Parallel work starts after packaging. Standard input is disabled, successful output is buffered unless live mode is requested, and depends only orders environments already selected for that invocation. It does not select dependencies automatically. Pin min_version or requires when configuration or plugins need a particular tox build. If the host command cannot satisfy those constraints, tox provisions another copy under .tox/.tox before the requested environments run, adding one more installation and cache to diagnose.
Patterns
Test one wheel on Python 3.12 and 3.13 create-toml-config
env_list = ['3.12', '3.13']
[env_run_base]
package = 'wheel'
deps = ['pytest>=8']
commands = [
['pytest', { replace = 'posargs', default = ['tests'], extend = true }],
]Both interpreters must already be discoverable; tox does not download the missing Python version.
Configure tox inside pyproject.toml configure-in-pyproject
[tool.tox]
env_list = ['3.12', '3.13']
[tool.tox.env_run_base]
package = 'wheel'
deps = ['pytest>=8']
commands = [
['pytest', { replace = 'posargs', default = ['tests'], extend = true }],
]The [tool.tox] prefix belongs only in pyproject.toml; standalone tox.toml places these keys at the root.
Cross Python and Django factors generate-dependency-matrix
env_list = [
{ product = [
['3.12', '3.13'],
['django52', 'django60'],
] },
]
[env_run_base]
deps = [
'pytest',
{ replace = 'if', condition = 'factor.django52', then = ['Django>=5.2,<6'], extend = true },
{ replace = 'if', condition = 'factor.django60', then = ['Django>=6,<6.1'], extend = true },
]
commands = [['pytest', 'tests']]This product creates 4 environments, and each conditional dependency extends pytest only for its matching factor.
Send pytest flags through tox run-one-environment
tox run -e 3.12 -- -k test_login -xArguments after the bare separator fill posargs; earlier flags are parsed by tox itself.
Inspect expanded environment settings inspect-resolved-config
tox list
tox config -e 3.12tox config resolves inheritance and substitutions, which exposes values placed in the wrong section.
Bound a parallel matrix run-matrix-in-parallel
tox parallel --parallel 4
tox parallel --parallel-live --parallel 2Parallel mode disables stdin; --parallel-live streams output instead of buffering successful environments.
Pass only reviewed host variables control-environment-variables
[env_run_base]
pass_env = ['CI', 'HTTP_PROXY']
disallow_pass_env = ['*_TOKEN']
set_env.PYTHONWARNINGS = 'error'
set_env.DATABASE_URL = 'sqlite:///{env_tmp_dir}/test.db'set_env can override passed values, and broad pass_env patterns risk copying credentials into every test environment.
Run Ruff without packaging the project add-lint-environment
env_list = ['3.12', 'lint']
[env.lint]
skip_install = true
deps = ['ruff']
commands = [
['ruff', 'check', '.'],
['ruff', 'format', '--check', '.'],
]skip_install changes this job to package mode skip, avoiding a source build and project installation.
Reuse one pure-Python wheel build reuse-wheel-build
[env_run_base]
package = 'wheel'
wheel_build_env = '.pkg'
deps = ['pytest']
commands = [['pytest', 'tests']]A shared wheel build fits pure Python; native extensions may require a different artifact for each interpreter.
Select static checks by label group-environments-by-label
env_list = ['3.12', 'ruff', 'mypy']
labels = { tests = ['3.12'], static = ['ruff', 'mypy'] }
[env.ruff]
skip_install = true
deps = ['ruff']
commands = [['ruff', 'check', '.']]
[env.mypy]
deps = ['mypy']
commands = [['mypy', 'src']]
# Run with: tox run -m statictox run -m static chooses the two named environments without duplicating them in the CI command.
Order coverage after selected tests order-coverage-aggregation
env_list = ['3.12', '3.13', 'coverage']
[env.coverage]
depends = ['3.*']
skip_install = true
deps = ['coverage']
commands = [['coverage', 'combine'], ['coverage', 'report']]depends controls ordering only among environments included in the run; selecting coverage alone does not add Python 3.12 or 3.13.
Provision a pinned tox toolchain pin-tox-and-plugin
min_version = '4.60'
requires = ['tox>=4.60,<5', 'tox-uv>=1,<2']
env_list = ['3.12']Unsatisfied constraints create .tox/.tox, install the requested tox and plugin versions there, and delegate the run.
Alternatives
| Package | Registry | Pick it when |
|---|---|---|
| nox | PyPI | Use it when Python functions express sessions and matrix logic more clearly than declarative TOML |
| hatch | PyPI | Use it when environments should share one tool with project building, versioning, and publishing |
| uv | PyPI | Use it when interpreter installation, locking, and fast commands matter more than a tox-style matrix |
More testing guides
pytest · chai · vitest · jsdom · playwright · coverage · the whole shelf →
How this guide is made: grounded in the library's documentation, release notes, changelog, and issue history, on a fixed rubric — not a hands-on install of every release. The 50 most-downloaded entries are additionally install-verified in clean containers. Corrections: contact the desk.

