Fix: Tox Not Working — Environment Creation, Config Errors, and Multi-Python Testing
Part of: Python Errors
Quick Answer
How to fix Tox errors — ERROR cannot find Python interpreter, tox.ini config parsing error, allowlist_externals required, recreating environments slow, pyproject.toml integration, and matrix env selection.
The Error
You run tox and it can’t find the Python interpreter:
ERROR: InterpreterNotFound: python3.11Or the config fails to parse:
ERROR: tox.ini: No section: 'testenv'
py311: remove tox env folder ...Or a command in commands fails with a confusing security warning:
WARNING: test command found but not installed in testenv
cmd: /usr/bin/make
env: py311Or tox creates environments correctly but they take forever:
GLOB sdist-make: /project/setup.py
py311 create: /project/.tox/py311
py311 installdeps: -rrequirements.txt
# 10 minutes later, still runningOr the tox 3 → 4 migration breaks custom configs:
tox 4 removed `install_command`. Use `install_command` inside `[testenv]` only.Tox automates testing across multiple Python versions and environments — you define a matrix (e.g., py39, py310, py311 × sqlite, postgres), and Tox creates virtualenvs, installs dependencies, and runs your test command in each. Essential for library maintainers but the config has subtleties. Tox 4 (released December 2022) reorganized several behaviors, breaking some Tox 3 configs. This guide covers each common failure.
Why This Happens
Tox reads tox.ini (or pyproject.toml with [tool.tox]), creates one virtualenv per environment, installs the project plus dependencies, and runs your specified commands. Environment creation relies on finding Python binaries by version — if python3.11 isn’t on PATH, Tox can’t create the py311 env.
Tox 4 tightened security: external commands (binaries outside the env) must be explicitly allowlisted. This caught many users whose Tox 3 configs ran make, git, or custom scripts without declaration.
Diagnostic Timeline
When tox fails locally but worked yesterday — or works locally but fails in CI — the first instinct is “delete .tox and re-run.” That hides the actual fault about half the time. Walk this timeline first.
Minute 0 — Wrong first instinct. You run rm -rf .tox && tox, watch the install run for five minutes, and end up with the same failure. Deleting .tox only re-tests whether the create step is deterministic. It does not change interpreter resolution, plugin precedence, or environment matrix selection — and those are where the real bugs hide.
Minute 1 — Discriminating evidence. Run tox --showconfig -e <failing_env> and read three lines: base_python, package, and commands_pre + commands. If base_python resolves to a different Python than you expect, you have an interpreter mismatch. If package = wheel but you expected editable, your build backend is wrong. If commands_pre shows a step you never wrote, a plugin (often tox-uv or tox-gh) injected it.
Minute 2 — Next check. Inspect plugin precedence with tox --version. The output lists every loaded plugin and version. The single most common Tox 4 surprise is resolver drift: tox-uv resolves dependencies via uv while a coworker’s machine uses pip, producing different lock states. If a CI run has tox-uv installed but local does not, you will get different transitive packages with no obvious clue. pip list | grep tox on both sides catches this in seconds.
Minute 3 — Actual root cause. Three causes account for most Tox 4 failures that survive a .tox wipe:
- tox-uv vs tox-pip resolution drift. Same
pyproject.toml, different resolver, different versions. Pin the resolver in[tox]viarequires = tox-uv>=1.11and commit the lock used in CI so everyone hits the same backend. - Isolated build backends. Tox 4 builds your package in an isolated env by default, so any build-time dep missing from
[build-system].requirescauses a confusing “module not found” during install, not during runtime. The error blames your code; the cause is yourpyproject.toml. - Environment matrix shadowing. Factor envs like
py311-postgresand a literal[testenv:py311-postgres]section both match — and the literal section silently overrides the factor logic. If a factor flag (postgres: psycopg) seems ignored, search for a literal env section with the same name.
If none of those fit, then wipe .tox. By then you actually know whether you are testing creation, resolution, or runtime.
Fix 1: InterpreterNotFound
ERROR: InterpreterNotFound: python3.11Tox looks for interpreters named python3.x on PATH. If python3.11 isn’t installed, that env can’t build.
Install the missing Python version:
# Via pyenv
pyenv install 3.11.9
pyenv local 3.11.9
# Via deadsnakes on Ubuntu
sudo add-apt-repository ppa:deadsnakes/ppa
sudo apt install python3.11 python3.11-venv
# Via uv (recommended — fast, zero-config)
uv python install 3.11
# Or Homebrew on macOS
brew install [email protected]Use tox-uv plugin to automatically download missing interpreters:
pip install tox-uv[tox]
requires = tox-uv>=1.11
env_list = py39, py310, py311, py312With tox-uv, missing Python versions are downloaded automatically — no manual install per version.
Skip missing interpreters in CI where only some are available:
tox --skip-missing-interpretersOr in config:
[tox]
skip_missing_interpreters = trueCommon Mistake: Running tox in CI and assuming it will auto-install Python versions. Tox on its own won’t — it only uses what’s already on PATH. Use tox-uv, actions/setup-python with matrix builds, or install versions explicitly in the CI workflow.
Fix 2: Basic tox.ini Structure
# tox.ini
[tox]
env_list = py39, py310, py311, py312, lint, type
[testenv]
deps =
pytest>=7
pytest-cov
commands = pytest --cov=myproject --cov-report=term {posargs}
[testenv:lint]
skip_install = true
deps = ruff>=0.5
commands = ruff check . --no-fix
[testenv:type]
deps =
mypy>=1.10
pydantic
commands = mypy src/Run specific environments:
tox # All envs in env_list
tox -e py311 # Just py311
tox -e lint,type # Multiple
tox -e py311 -- -v --tb=short # Pass args to pytest via {posargs}Environment inheritance — [testenv:name] inherits from [testenv], override only what you need:
[testenv]
deps = pytest
commands = pytest
[testenv:integration]
deps =
{[testenv]deps}
requests
docker
commands = pytest -m integration
[testenv:py311-redis]
deps =
{[testenv]deps}
redis
set_env = REDIS_URL=redis://localhost:6379
commands = pytest -m redisTox 4 vs Tox 3 — key syntax changes:
| Tox 3 | Tox 4 |
|---|---|
envlist | env_list (hyphen → underscore) |
setenv | set_env |
whitelist_externals | allowlist_externals |
basepython = python3.11 | Same (but now matches py311 by default) |
Top-level section was [tox] | Same |
The old names still work for backward compatibility, but they emit warnings. Migrate to the new names for clean output.
Fix 3: allowlist_externals for External Commands
WARNING: test command found but not installed in testenv
cmd: makeTox 4 requires external commands (commands not installed in the virtualenv) to be explicitly allowlisted. This prevents accidental shell injection and surprise command resolution.
[testenv]
allowlist_externals =
make
docker
git
commands =
make build
docker compose up -d
pytestCommon externals:
allowlist_externals =
bash
sh
make
docker
docker-compose
git
grep
rmWhy this matters — without the allowlist, Tox fails silently in Tox 3 (running the command anyway with a warning) and loudly in Tox 4 (refusing to run). Tox 4’s stricter behavior catches real bugs, but broke many migrating projects.
Pro Tip: Instead of allowlisting shell utilities, prefer Python-based alternatives where possible. Use pytest-cov instead of coverage CLI, ruff instead of separate flake8 + isort + black binaries. Python tools installed in the testenv don’t need allowlisting. The fewer external commands, the more portable your tox config.
Fix 4: pyproject.toml Integration
Tox 4 supports configuration directly in pyproject.toml:
# pyproject.toml
[tool.tox]
requires = ["tox>=4"]
env_list = ["py311", "py312", "lint"]
[tool.tox.env_run_base]
deps = ["pytest>=7"]
commands = [["pytest", "{posargs}"]]
[tool.tox.env.lint]
skip_install = true
deps = ["ruff>=0.5"]
commands = [["ruff", "check", "."]]The TOML format is more verbose than INI but integrates cleanly with modern Python packaging (where pyproject.toml holds everything).
Migration from tox.ini — the parser auto-detects and runs either format. Keep both during a transition period, then delete tox.ini once pyproject.toml is verified.
Fix 5: Factor-Conditional Config
Factors let you express matrix variations compactly:
[tox]
env_list = py{39,310,311,312}-{sqlite,postgres}-{linux,macos}
[testenv]
deps =
pytest
sqlite: # no extra deps
postgres: psycopg[binary]>=3
set_env =
sqlite: DATABASE_URL=sqlite:///test.db
postgres: DATABASE_URL=postgresql://localhost/test
linux: OS=linux
macos: OS=macos
commands = pytest {posargs}This creates 16 combinations: 4 Python × 2 DB × 2 OS. Run a specific combination:
tox -e py311-postgres-linuxFactor logic — conditional deps and commands:
[testenv]
deps =
pytest
# Only for Python 3.11+
py311,py312: pydantic>=2
# Only for Python 3.9, 3.10
py39,py310: pydantic<2Common Mistake: Putting factor syntax in the wrong position. Factors apply at the line level — sqlite: psycopg means “this line only for sqlite envs”. If you write deps = sqlite: psycopg, that whole line applies only to sqlite (other envs get no deps).
Fix 6: Speed Up Environment Creation
py311 installdeps: -rrequirements.txt
# Takes 5 minutes per Python version to reinstallTox creates fresh environments every time by default. Speed it up:
Cache environments via recreate = false (default behavior since Tox 4):
[testenv]
recreate = false # Reuse if config hasn't changedUse tox-uv plugin for dramatically faster installs:
pip install tox-uv[tox]
requires = tox-uv>=1.11uv’s Rust-based package resolver and installer is 10–100x faster than pip. For a typical test suite, this turns a 5-minute tox run into 30 seconds.
Parallel execution:
tox -p auto # Run all envs in parallel (auto-detect worker count)
tox -p 4 # 4 workers
tox --parallel-no-spinner # No progress spinner (better for CI logs)Parallel mode runs each env concurrently. 4-env matrix on a 4-core machine completes in roughly the same time as a single env (when packages are cached).
CI caching — GitHub Actions example:
- uses: actions/cache@v4
with:
path: |
.tox
~/.cache/pip
~/.cache/uv
key: tox-${{ hashFiles('tox.ini', 'pyproject.toml', '**/*.txt') }}
- run: pip install tox tox-uv
- run: tox -p autoFix 7: Passing Environment Variables
Tox runs commands in an isolated environment — no shell variables pass through by default. Explicitly forward variables you need:
[testenv]
pass_env =
CI
GITHUB_*
CODECOV_TOKEN
DATABASE_URL
set_env =
PYTHONDONTWRITEBYTECODE = 1
COVERAGE_FILE = {envtmpdir}/coveragepass_env — forward existing env vars from the host. set_env — set new env vars.
Common vars to pass:
pass_env =
CI # Indicates running in CI
GITHUB_ACTIONS # GitHub Actions specific
CODECOV_* # Codecov tokens
COVERAGE_* # Coverage.py config
HOME # User home (some tools fail without)
USER
LANG
LC_*Docker-in-docker scenarios often need:
pass_env =
DOCKER_HOST
DOCKER_CERT_PATH
DOCKER_TLS_VERIFYFix 8: Coverage Combining Across Environments
When running tests across multiple Python versions, combining coverage from all envs gives you total code coverage.
[testenv]
deps =
pytest
pytest-cov
coverage>=7
set_env =
COVERAGE_FILE = {envtmpdir}/coverage.{envname}
commands =
coverage run -m pytest {posargs}
coverage xml -o {envtmpdir}/coverage.xml
[testenv:coverage-report]
skip_install = true
deps = coverage>=7
depends =
py39, py310, py311, py312
commands =
coverage combine .tox/*/tmp/.coverage.*
coverage report
coverage html -d htmlcovtox
tox -e coverage-report # Combines results from all py envs.coveragerc or pyproject.toml coverage config:
[tool.coverage.run]
source = ["src"]
parallel = true # Required for combining across envs
[tool.coverage.report]
exclude_lines = [
"pragma: no cover",
"def __repr__",
"raise NotImplementedError",
]If a coverage env reports 0% despite running tests, the most common cause is parallel = true set in your coverage config without a coverage combine step — each env writes its own .coverage.<host>.<pid> file and your report runs against the wrong one.
Still Not Working?
Tox vs Nox
- Tox — Config-driven (INI/TOML), declarative, mature ecosystem, great for standard test matrices.
- Nox — Python-configured (noxfile.py), programmatic, easier for conditional logic and dynamic envs.
Both solve the same problem (cross-version testing) with different philosophies. Use tox for simple matrices; use Nox when your session logic needs Python control flow (loops, conditionals, external resource checks).
Isolated Build Backend
Tox builds your package in an isolated environment before installing — this catches setup.py or pyproject.toml bugs that local development hides:
[testenv]
package = wheel # Build as wheel (default; faster than sdist for reuse)
# Or: package = sdist | editable | skipeditable — install with pip install -e ., so code changes are reflected without rebuild:
[testenv:dev]
package = editable
deps = pytest-watch
commands = ptwskip — don’t install the package at all (useful for lint/format envs):
[testenv:lint]
skip_install = true
deps = ruff
commands = ruff check .Skipping install is faster for envs that don’t actually need your project importable.
Pre-Commit and Tox Together
Tox orchestrates the test matrix; pre-commit runs format/lint checks on every commit. A common setup:
[testenv:precommit]
skip_install = true
deps = pre-commit>=3
commands = pre-commit run --all-files
[testenv:test]
deps = pytest
commands = pytest
# Default envs run both
[tox]
env_list = precommit, py311, py312Run via tox — all envs run in sequence (or parallel with -p). In CI this becomes a single command that covers the whole quality pipeline.
GitHub Actions Matrix with Tox
Two approaches — let tox handle the matrix, or let GitHub Actions:
GitHub Actions matrix (recommended):
strategy:
matrix:
python-version: ["3.9", "3.10", "3.11", "3.12"]
steps:
- uses: actions/setup-python@v5
with:
python-version: ${{ matrix.python-version }}
- run: pip install tox tox-gh
- run: toxWith tox-gh, tox runs only the env matching the current GH Actions Python version. Parallelism comes from GH Actions running multiple matrix jobs.
Tox handles matrix:
- uses: actions/setup-python@v5
with:
python-version: "3.12"
- run: pip install tox tox-uv
- run: tox -p auto # tox-uv fetches all other Python versionsSimpler CI config but one long-running job instead of parallel jobs.
Debugging Failed Environments
# Verbose output
tox -v
tox -vv # Very verbose
tox -vvv # Even more verbose
# Show the command that would run without executing
tox --showconfig -e py311
# Skip tests, just install
tox -e py311 --notest
# Enter the env shell for manual debugging
.tox/py311/bin/python -c "import myproject; print(myproject.__file__)"Integration with pytest and Ruff
Tox is almost always used with pytest for the actual testing. For pytest-specific errors, see pytest fixture not found. For Ruff-based linting that’s commonly orchestrated via a [testenv:lint] environment, see Ruff not working.
Plugin Ecosystem
Popular Tox plugins worth knowing:
tox-uv— Use uv for blazing-fast installstox-gh— Dynamic env selection in GitHub Actionstox-factor— Run envs matching a specific factor (tox -f py311)tox-extra— Additional options like environment listingtox-pyenv— Auto-detect pyenv-installed versions
For uv setup and configuration that pairs with tox-uv, see uv not working. For pre-commit integration that complements tox in CI, see pre-commit not working.
Tox 4 Behaves Differently in CI Than Locally
Same tox.ini, same Python version, different results. The usual suspects: a pass_env list that includes a variable set locally but absent in CI (or vice versa), a tox-gh plugin installed in CI that narrows the env list to just one factor, or a cached .tox directory restored from a previous Python version. Print the effective config in both places with tox --showconfig -e <env> and diff the output. The first divergent line is almost always your fault.
pyproject.toml Tox Config Is Ignored
Tox 4 reads [tool.tox] from pyproject.toml, but only if there is no tox.ini in the same directory. A stale tox.ini (even an empty one left from a migration) takes precedence and your TOML changes silently do nothing. Delete tox.ini once you have migrated and verify with tox --showconfig that the values come from the TOML file.
Trying to Test on a Python Version That Is Not Installed
tox does not install Python interpreters by itself. pyenv local 3.12 is not enough — Tox needs python3.12 resolvable on PATH, which pyenv does not always provide depending on shims. Either install tox-uv (which downloads missing versions) or run pyenv shell 3.12 before tox. In CI, use actions/setup-python@v5 with a matrix; never rely on the runner image to ship the version you want.
Solo developer based in Japan. Every solution is cross-referenced with official documentation and tested before publishing.
Was this article helpful?
Related Articles
Fix: Nox Not Working — Session Errors, Virtualenv Backends, and Reuse Logic
How to fix Nox errors — no noxfile.py found, session not detected, virtualenv backend uv not installed, session.install fails outside virtualenv, parametrize matrix exploding, and reuse_venv confusion.
Fix: freezegun Not Working — Datetime Not Frozen, Timezone Issues, and Async Tests
How to fix freezegun errors — freeze_time decorator not affecting datetime.now, timezone-aware datetime mismatch, time.time not frozen, async test time leak, third-party library still using real time, and tick parameter behavior.
Fix: Moto Not Working — Mock Decorator, Real AWS Calls Leaking, and v4 to v5 Migration
How to fix Moto errors — mock not activating, real AWS credentials used in tests, ImportError mock_s3 removed in v5, fixtures with multiple services, NoCredentialsError despite mock, and standalone server mode.
Fix: Hypothesis Not Working — Strategy Errors, Flaky Tests, and Shrinking Issues
How to fix Hypothesis errors — Unsatisfied assumption, Flaky test detected, HealthCheck data_too_large, strategy composition failing, example database stale, settings profile not found, and stateful testing errors.