Quick Reference · Python test automation & env orchestration

tox cheat sheet4.x

One config file names a set of environments. For each one, tox builds an isolated virtualenv, installs your package and its deps, and runs your commands — so “works on my machine” becomes “works on every Python you list.” Learn the loop once and the whole matrix follows.

CLI & run [tox] core [testenv] settings factors & substitutions modern / uv plugins & CI gotcha / legacy most common

Verified against tox 4.58.0 (installed & run, Jul 2026) · every config key read from tox config, every flag from tox --help · tox.wiki config + user guide · tox-uv docs. Factor expansion, substitutions & the result summary were confirmed by execution, not recall. Re-verified 2026-08-30: tox 4.61.1 (28 Aug 2026) current, Python 3.10+ — INI format deprecated in 4.59; {home}/{tox_root_name} added in 4.60.

One config → a matrix of environments → the same four steps in each
ONE CONFIG tox.toml / tox.ini [tox] env_list = py3.{11,12,13}, lint [testenv] deps = pytest commands = pytest expand EXPANDS INTO A MATRIX OF ENVIRONMENTS py3.11 py3.12 py3.13 lint each is an isolated venv for every environment, run the same four steps ↓ INSIDE ONE ENVIRONMENT (e.g. py3.12) 1 · create venv base_python picks the interpreter → virtualenv .tox/py3.12/ 2 · install deps (install_command) + your package (sdist/wheel) unless skip_install 3 · run commands commands_pre → commands → commands_post in change_dir, with set_env 4 · report exit 0 → OK non-zero → FAIL timed: setup[..]+cmd[..] tox -r / recreate=true throws the venv away and rebuilds from step 1 RESULTS AGGREGATE INTO ONE SUMMARY py3.11: OK (12.3=setup[9.1]+cmd[3.2] seconds) py3.12: OK (11.8 seconds) lint: FAIL code 1 (2.1 seconds) evaluation failed :( — one red env fails the whole run (exit 1)
The same project, both config dialects
tox.ini · the classic INI format
[tox]
requires = tox>=4.22
env_list = py3.{11,12,13}, lint

[testenv]              # defaults for every env
description = run the tests
deps = pytest>=8
commands = pytest {posargs:tests}

[testenv:lint]       # one named override
skip_install = true
deps = ruff
commands = ruff check .
tox.toml · native TOML (tox 4.21+)
requires = ["tox>=4.22"]
env_list = ["py3.13", "py3.12", "lint"]

[env_run_base]       # the [testenv] equivalent
description = "run the tests"
deps = ["pytest>=8"]
commands = [["pytest", { replace = "posargs",
  default = ["tests"], extend = true }]]

[env.lint]           # one named override
skip_install = true
deps = ["ruff"]
commands = [["ruff", "check", "."]]
01Install & First Runget moving
02What tox Actually Doesper environment
03[tox] Core Settingswhole-project
04[testenv] Essentialsthe daily keys
05Environment & Shellwhat the command sees
06Factors & Generative Envsthe matrix engine
07Substitutions{curly} replacements
08Sharing & DRY Configdon't repeat
09Running & Selectingthe CLI
10Parallel & Speedfaster matrices
11Inspect & Debugwhat will run?
12Where Config Livesfour options
13Packaging the Projectwhat gets installed
14Skips & Toleranceswhen envs may miss
15tox + uv & PEP 723the fast lane
16tox in CIGitHub Actions
17Plugins & Provisioningextend tox
18Common Env Recipescopy-paste starts
19tox 3 → 4 & Gotchaswhat bites
Reading the Resultthe summary line

Four things worth seeing once

The factor grid explains why one line becomes eight envs; the isolation model explains why tox catches bugs your local venv hides. Both are drawn from behaviour verified against tox 4.58.0.

factors multiply into a matrix

Two brace-groups in one env name become the Cartesian product. This single line defines six real environments.

env_list = py3.{11,12,13}-django{42,52} django42 django52 py3.11 py3.12 py3.13 py3.11-django42 py3.11-django52 py3.12-django42 py3.12-django52 py3.13-django42 py3.13-django52 3 × 2 = 6 envs. django42: Django>=4.2 then applies to just the left column.

why the isolated venv matters

tox builds your package and installs it into a clean env. A missing dependency that your dev venv happens to have installed fails here — on purpose.

your dev venv your package (editable) pytest, requests ipython, black, pip-tools that thing from 6 months ago a transitive dep, by luck tests pass ✓ even the undeclared import tox env (.tox/py312) your package (built wheel) exactly what deps = lists nothing else ModuleNotFoundError ✗ the bug your users would hit

where each key belongs

The single most common config mistake: a per-env key placed in [tox]. It isn't an error — it's silently ignored.

[tox] · core affects the whole project env_list requires min_version no_package deps = pytest ← ignored! [testenv] · per-env defaults for every env deps commands set_env / pass_env skip_install change_dir, extras… tox config tells you where a key landed

serial vs parallel

By default envs run one after another. -p runs them at once; depends still enforces ordering where it matters.

tox — serial (default) py3.11 py3.12 py3.13 coverage wall time → long tox -p — parallel py3.11 py3.12 py3.13 coverage depends = py3.11, py3.12, py3.13 the three run at once; coverage waits for them

Worth memorizing

factors multiply{a,b}-{x,y} = 4 envs, Cartesian product
-e vs -f vs -mexact envs · by factor · by label
tox config -kthe fastest way to see what a key resolves to
key in wrong sectiontestenv key in [tox] is silently ignored
-- passes posargseverything after -- lands in {posargs}
stale env?tox -r recreates — edits to deps need it
externalsallowlist_externals, not the old whitelist
env is scrubbedneed $VAR? add it to pass_env
tests the wheeltox installs a built package, not your source tree
skip_installfor lint/docs envs that don't import the project
TOML has no factorsgenerative names & conditionals are INI-only
tox-uvdrop-in uv backend — much faster envs
requires =auto-provisions plugins & a newer tox
depends =ordering for parallel — e.g. coverage last
exit 0 vs 1one failed env fails the whole run
.tox/ is disposablegit-ignore it; delete to reset