pipx install tox★Install once, globally isolated — the recommended way. Oruv tool install tox.pip install toxInside a project venv works too. Needs Python 3.8+ to run tox itself.tox --versionPrints tox's version and every loaded plugin.tox★Run every env inenv_list, in order. This istox run(akatox r).tox -e py312★Run just one env. Comma-separate for several:-e py311,py312.tox quickstartAnswer a few prompts, get a starter config written for you.
# 1 create an isolated venv★One per env, under.tox/<name>/. Never touches your working venv.# 2 build + install your package★Builds an sdist/wheel once, installs it into each env — tests the installed code, not the source tree.# 3 install deps, run commands★Your test/lint/build commands, in a clean shell with a controlled environment.# 4 report OK / FAIL per env★Any non-zero exit fails the env; any failed env fails the run..tox/All the envs live here. Delete it and tox rebuilds from scratch. Git-ignore it.
env_list = py313, py312, lint★Which envs a baretoxruns. (INI's old nameenvliststill works.)requires = tox>=4.22★Bootstraps a provision env if the running tox is too old, or a plugin is missing.min_version = 4.0Hard floor — error out if tox is older. (INI:minversion.)skipsdist = trueDon't build the project at all — for apps with no package to install.no_package = trueThe tox 4 name for the same idea. Pairs well with per-envskip_install.work_dir = .toxWhere envs are built. Rarely changed.these keys go in [tox] onlyPut a testenv key here by mistake and it's silently ignored.tox configcatches it.
deps = pytest>=8★Test dependencies, one per line. Anything pip understands: pins, extras, URLs,-r reqs.txt.commands = pytest {posargs:tests}★What to run. Each line is one command; non-zero exit fails the env.description = run the unit tests★Shown bytox list. Cheap documentation — always add one.basepython = python3.12Force the interpreter. Usually inferred from the env name (py312→ 3.12).skip_install = true★Don't install your project — for lint/format/docs envs that don't import it.package = wheel # | sdist | editable | skipHow to build the project under test.editable=pip install -e.extras = test,docsInstall your project's optional-dependency groups from pyproject.commands_pre / commands_postRun before/after the main commands._postruns even if commands fail.
set_env =★
PYTHONPATH = {toxinidir}/srcVars set inside the env. (INI:setenv.) Multi-line,KEY = value.pass_env = HOME, CI, TERM★tox scrubs the environment; only these pass through. Globs allowed:PIP_*.pass_env = *Let everything through. Blunt, but common in messy CI.change_dir = testscd here before running commands. (INI:changedir.)allowlist_externals = make, bash★Permit non-Python commands. Without it, external tools warn then fail.install_command = uv pip install {opts} {packages}Override how deps get installed — e.g. swap pip for uv.setenv: secrets get redactedVars named like*token*/*secret*/*key*are logged as***.4.x
env_list = py3.{11,12,13}★Braces expand: this is three envs. The pieces (py3.11,12…) are factors.{py311,py312}-django{42,52}★Two axes multiply → four envs: py311-django42, py311-django52, py312-…django42: Django>=4.2,<4.3★Factor-conditional dep: applies only in envs whose name containsdjango42.py311: coverage<7Any setting can be factor-conditional, not just deps.!lint: pytestNegation — applies to every env except ones with thelintfactor.py3.{11-13}Range form — same three envs. Open-ended3.{10-}works too.4.x{py312,py313}-{a,b}: depMultiple factors on one line = AND. All named factors must be present.TOML has no factor syntaxGenerative names & conditional settings are INI-only. TOML uses explicit range dicts instead.TOML
{posargs}★Extra CLI args after--.tox -e py312 -- -k slowfeeds-k slowhere.{posargs:tests}★Default when none given — runstestsif the user passes nothing.{env:VAR}·{env:VAR:fallback}★Read an env var, with an optional default if it's unset.{toxinidir}·{work_dir}Project root (where the config lives) · the.toxdir.{env_dir}·{env_name}This env's directory · its name — handy in coverage file names.{[testenv]deps}★Pull a value from another section — the DRY trick for sharing deps.{tty:ON:OFF}·{/}·{:}Ternary on interactive terminal · OS path sep · OS path-list sep.{ replace = "posargs", default = […] }The TOML spelling of the same substitutions — a dict, not a{curly}string.TOML
[base]★
deps = pytest
mockA non-testenv section to hold shared values.[testenv]★
deps = {[base]deps}
pytest-covReference it, then add more. This is how most real configs stay tidy.[testenv:docs]
deps = {[testenv:lint]deps}Reuse another env's deps verbatim.[testenv]
base = {env_run_base}TOML:[env.foo]inherits from[env_run_base]automatically.generative section headers[testenv:{py311,py312}-lint] defines both at once, factor rules and all.
tox run -e py312,lint★Run specific envs.runis the default subcommand, sotox -eis enough.tox -e ALLEvery env in the config, including ones not inenv_list.tox -m testRun by label — envs that declaredlabels = test. Great for grouping.tox -f py312Run every env whose name contains thepy312factor.tox -- -k test_login -x★Everything after--becomes{posargs}inside the commands.tox -r★Recreate envs from scratch — the fix for “stale venv” weirdness.tox -n·tox --notestBuild & install but skip the commands — just provision the envs.tox --fail-fastStop at the first env that fails instead of running the rest.4.xtox -x testenv.package=wheelOverride any config key from the CLI, no file edit.
tox -p·tox run-parallel★Run all envs at once.-p autosizes to your CPU count.tox -p auto -o-o/--parallel-no-spinnerstreams live output instead of a spinner.parallel_show_output = truePer-env: always print this env's output, even when it passes in parallel.depends = py311, py312★Ordering hint for parallel runs — e.g. makecoveragewait for the test envs.tox --skip-env-installReuse an existing env, skip both dep and package install — fast reruns.4.xparallel + shared tmpGive each env its own temp dir (--basetemp={env_tmp_dir}) or parallel pytest runs collide.
tox list·tox -l★List envs with their descriptions, default vs additional.tox config★Print the fully-resolved config — after factors, substitutions, inheritance.tox config -e py312 -k deps commands★Just the keys you care about, for one env. Your first debugging move.tox dependsShow the env dependency graph (fromdepends =).tox -v·tox -vvMore verbosity — see the exact pip and command invocations.tox exec -e py312 -- pythonDrop a one-off command into a built env — e.g. an interactive shell.tox devenv -e py312 .venvMaterialize an env as a normal dev virtualenv you can activate.tox --result-json out.jsonMachine-readable results for CI dashboards.
tox.ini → [tox] / [testenv]★The classic. Full factor + substitution syntax. Most examples online use this.tox.toml → root keys / [env_run_base]★Native TOML. Root-level keys are core;[env.NAME]per env.4.21+pyproject.toml → [tool.tox]★Keep everything in one file. Same native TOML shape under[tool.tox].4.21+pyproject.toml → legacy_tox_iniEmbed an INI string in TOML — the bridge before native TOML existed.setup.cfg → [tox:tox]Still read, rarely used now.tox -c path/to/tox.iniPoint at a specific config instead of auto-discovering one.no factors in TOMLTOML can't do{py311,py312}names orfactor:conditionals. Stay on INI if you lean on those.TOML
package = wheel★Default. Builds one wheel, installs it into every run env — the point of tox.package = editablepip install -e— fast iteration, but tests the source tree not a built artifact.package = sdistBuild + install from source dist — the most faithful to what users get.wheel_build_env = .pkgBuild the wheel once in a shared env, reuse across the matrix.[tox]
requires = tox>=4Build backend comes from yourpyproject.toml[build-system], PEP 517-style.tox --installpkg dist/pkg.whlSkip building — install a prebuilt artifact (test the exact release wheel).tox --sdistonly·tox -bOnly do the packaging step.
[tox]★
skip_missing_interpreters = trueDon't fail the run if some Python isn't installed — skip that env. Default isconfig.tox -s trueSame, from the CLI, overriding the config.ignore_errors = trueRun every command even if an earlier one fails; report failure at the end.ignore_outcome = trueTurn this env's failure into a warning — for allowed-to-fail envs (e.g. nightly).platform = linuxSkip the env unlesssys.platformmatches this regex.- pip install ...A command prefixed with-may fail without failing the env (make-style).
pip install tox-uv★Swap virtualenv+pip for uv — dramatically faster env creation and installs.uvrunner = uv-venv-lock-runnerInstall from auv.lockviauv sync— fully reproducible envs.uvrunner = uv-venv-pep-723Run a standalone script by its inline metadata — uv-backed.uvrunner = virtualenv-pep-723Built into tox 4.52+: read a script'srequires-python+dependenciesheader, nodepsneeded.4.52+[testenv:check]
script = tools/check.pyWith a PEP 723 runner, point at the script — deps come from its# /// scriptblock.4.52+TOX_UV_NO_PEP723=1Opt out of uv auto-backing the built-in PEP 723 runner.uv
pip install tox-gh★Maps the CI's Python to the right tox factor automatically.CItox -e pyThe barepyfactor uses whatever Python is running — perfect for a matrix job.strategy:★
matrix:
python: ['3.11','3.12','3.13']One CI job per Python; each runs its slice of the tox matrix.tox -e py -- --cov --cov-report=xmlPass coverage flags throughposargs; upload the XML afterward.FORCE_COLOR=1Keep tox's colours in CI logs (it's in the defaultpass_env).tox -p -oParallel with streamed output reads well in CI logs.
requires = tox-uv>=1.13★List a plugin here and tox auto-provisions a bootstrap env with it installed.tox-gh·tox-uv·tox-ini-fmtCI Python mapping · uv backend · format & sort your tox.ini.tox-docker·tox-condaSpin up service containers per env · use conda envs instead of virtualenv.tox --no-provisionFail loudly instead of bootstrapping — useful to detect missing plugins in CI.@impl def tox_add_option(parser)tox 4 plugins are pluggy hooks — add CLI options, envs, or config keys.tox --versionConfirms which plugins actually loaded.
[testenv:lint]★
skip_install = true
deps = ruff
commands = ruff check .Lint without installing the project — fast.[testenv:type]
deps = mypy
commands = mypy srcType-check. Installs the project so mypy sees your code.[testenv:docs]
deps = sphinx
commands = sphinx-build -W docs docs/_buildBuild docs;-Wturns warnings into errors.[testenv:coverage]★
depends = py311, py312
commands = coverage combine
coverage reportAggregate coverage after the test envs finish.[testenv:format]
commands = ruff format .A writing env — run it locally, not in CI's check step.
whitelist_externalsrenamedNowallowlist_externals. The old name is gone in tox 4.install_command must keep {packages}Override it and drop{opts} {packages}and nothing installs.a testenv key in [tox]Silently ignored — no error. Runtox configto see where a key actually landed.external command, no allowlisttox 4 warns then fails. Listmake,bash,gitinallowlist_externals.env sees a var you didn't passtox scrubs the environment — if a command needs$FOO, add it topass_env.config edits not taking effectChangeddeps? Recreate:tox -r. tox reuses envs aggressively.tox 4 is a full rewriteFaster and stricter than tox 3. Most configs port cleanly, but check externals + ignored keys.
py312: OK (11.8 seconds)env passed; total wall timeOK (12.3=setup[9.1]+cmd[3.2] s)time split: env build vs your commandspy312: FAIL code 1a command exited non-zeroSKIP: py310interpreter missing + skip_missing_interpreterscongratulations :)every env passed — exit 0evaluation failed :(at least one env failed — exit 1.pkg: _optional_hooks…the shared build env doing PEP 517 packaging