Fix: Tox Not Working — Environment Creation, Config Errors, and Multi-Python Testing
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.
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",
]For pytest coverage patterns that pair with tox, see pytest fixture not found.
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.
Solo developer based in Japan. Every solution is cross-referenced with official documentation and tested before publishing.
Was this article helpful?
Related Articles
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.
Fix: Locust Not Working — User Class Errors, Distributed Mode, and Throughput Issues
How to fix Locust errors — no locustfile found, User class not detected, worker connection refused, distributed mode throughput lower than single-node, StopUser exception, FastHttpUser vs HttpUser, and headless CSV reports.
Fix: Selenium Not Working — WebDriver Errors, Element Not Found, and Timeout Issues
How to fix Selenium errors — WebDriverException session not created, NoSuchElementException element not found, StaleElementReferenceException, TimeoutException waiting for element, headless Chrome crashes, and driver version mismatch.
Fix: pytest fixture Not Found – ERRORS or 'fixture not found' in Test Collection
How to fix pytest errors like 'fixture not found', 'ERRORS collecting test', or 'no tests ran' caused by missing conftest.py, wrong scope, or import issues.