Skip to content

Fix: Tox Not Working — Environment Creation, Config Errors, and Multi-Python Testing

FixDevs ·

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.11

Or 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: py311

Or 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 running

Or 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.11

Tox 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, py312

With 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-interpreters

Or in config:

[tox]
skip_missing_interpreters = true

Common 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 redis

Tox 4 vs Tox 3 — key syntax changes:

Tox 3Tox 4
envlistenv_list (hyphen → underscore)
setenvset_env
whitelist_externalsallowlist_externals
basepython = python3.11Same (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: make

Tox 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
    pytest

Common externals:

allowlist_externals =
    bash
    sh
    make
    docker
    docker-compose
    git
    grep
    rm

Why 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-linux

Factor 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<2

Common Mistake: Putting factor syntax in the wrong position. Factors apply at the line levelsqlite: 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 reinstall

Tox 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 changed

Use tox-uv plugin for dramatically faster installs:

pip install tox-uv
[tox]
requires = tox-uv>=1.11

uv’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 auto

Fix 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}/coverage

pass_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_VERIFY

Fix 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 htmlcov
tox
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 | skip

editable — install with pip install -e ., so code changes are reflected without rebuild:

[testenv:dev]
package = editable
deps = pytest-watch
commands = ptw

skip — 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, py312

Run 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: tox

With 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 versions

Simpler 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 installs
  • tox-gh — Dynamic env selection in GitHub Actions
  • tox-factor — Run envs matching a specific factor (tox -f py311)
  • tox-extra — Additional options like environment listing
  • tox-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.

F

FixDevs

Solo developer based in Japan. Every solution is cross-referenced with official documentation and tested before publishing.

Was this article helpful?

Related Articles