Supporting several Python versions requires running the suite on every claimed version. tox creates isolated environments and invokes consistent commands for tests, linting, and type checking.

Create tox.toml

requires = ["tox>=4.20"]
env_list = ["3.13", "3.12", "lint"]

[env_run_base]
description = "run tests"
deps = ["pytest"]
commands = [["pytest", "tests"]]

[env.lint]
skip_install = true
deps = ["ruff"]
commands = [["ruff", "check", "."]]

Run tox or select one environment with tox -e 3.13. The matching interpreter must exist. A skipped environment is not a successful compatibility test.

Use the pytest guide to build the suite and GitHub Actions for Python to execute the complete matrix.

Keep tool commands and dependency constraints aligned between local and CI runs. Cache downloads for speed, but verify that every environment can be recreated from scratch.

The official tox documentation, accessed July 22, 2026, recommends TOML for new projects and documents environments, matrices, and packaging. tox supports a compatibility claim only when the matrix matches versions the project actually maintains.

What tox actually isolates

Each tox environment has its own virtual environment, dependencies, and explicit commands. That boundary prevents a globally installed package from making a test pass accidentally. During a run, tox reads the configuration, locates the requested interpreter, creates or reuses an environment, installs the project and declared dependencies, and then invokes the commands.

The isolation is particularly valuable for libraries. Code may work on a developer's Python version yet fail elsewhere because of syntax, dependency constraints, or standard-library changes. Applications benefit too: separate test, lint, and typing environments make failures reproducible without mixing tool dependencies.

tox does not automatically download every Python interpreter. Provision supported interpreters with the operating system's toolchain or the CI image, then inspect and select environments:

tox list
tox run -e 3.13

Read the error before enabling skip_missing_interpreters. Skipping unavailable versions can make local development convenient, but CI responsible for a compatibility promise should fail when a required interpreter is absent.

Install the project as users receive it

Run environments normally install the project package. This finds packaging defects that calling pytest from the repository can hide, including missing package data and imports that only work because the current directory is on the path. Keep build metadata in pyproject.toml and exercise the installed artifact.

Use skip_install = true only for tasks that do not import the project, such as formatting checks. For a non-package application, package = "skip" may be appropriate, but it should express the actual project design rather than conceal a broken build.

Test-only requirements belong in deps. A project that maintains a requirements file can install it consistently:

[env_run_base]
deps = [
  "-r requirements-test.txt",
]
commands = [
  ["pytest", "-q", { replace = "posargs", default = ["tests"], extend = true }],
]

Now tox -e 3.13 -- tests/test_api.py -x forwards a focused selection to pytest. The -- separator makes clear that the remaining arguments belong to the command rather than tox. A sensible default keeps routine runs short while preserving an easy debugging path.

Give each environment one responsibility

A compatibility matrix does not need to repeat every tool on every Python version. Test the versions the project claims to support, then run independent quality checks in descriptive environments:

env_list = ["3.11", "3.12", "3.13", "lint", "type"]

[env.lint]
skip_install = true
deps = ["ruff"]
commands = [["ruff", "check", "src", "tests"]]

[env.type]
deps = ["mypy"]
commands = [["mypy", "src"]]

This layout gives precise feedback: a failure in type is not mistaken for a Python 3.11 incompatibility. It also avoids running the same lint command three times. If a tool must inspect or import the installed project, do not enable skip_install for that environment.

Factors and generated names help with a large matrix, but start with readable configuration. A little repetition is safer than a compact expression that maintainers cannot confidently expand.

Recreate environments and diagnose failures

tox reuses environments to speed up later runs and recreates them when relevant configuration changes. When debugging dependency state, request a clean rebuild:

tox run -r -e 3.13
tox run -e 3.13 -- -vv

The first command recreates the environment. The second can forward pytest verbosity when posargs is configured. Avoid deleting directories as the first response; tox output identifies the selected interpreter, installation step, and failing command.

An environment creation error is not a test failure. Check interpreter availability, package build output, dependency resolution, and finally command output. If tests pass outside tox, compare environment variables and working directories. Pass required non-secret variables deliberately, and design unit tests so they do not require a developer's credentials.

Choose a CI matrix strategy

CI can provision several interpreters in one job and run tox, or the platform can fan out a version matrix and invoke one tox environment per job. Fan-out provides parallelism and identifies the failing Python version immediately. A single tox run keeps more orchestration logic in one configuration. Both are valid if the same environments run locally.

Do not maintain unrelated compatibility lists in CI and tox. Align them with the project's stated support policy. Add a newly released Python version to the support claim only after it passes regularly. Removing an environment should likewise reflect an intentional support decision, not an attempt to hide a stubborn failure.

Cache downloads or environments only when the key accounts for Python version, operating system, and dependency files. A stale cache can produce misleading success or failures that cannot be reproduced. Schedule an occasional uncached run to prove that all environments remain buildable from scratch.

Keep the matrix trustworthy

Constrain dependencies where reproducibility requires it, while running a scheduled job against newer allowed releases to discover upcoming incompatibilities. Keep ordinary test commands deterministic and offline where practical. Integration tests that contact services deserve a separate environment and explicit credentials policy.

Use allowlist_externals sparingly. System executables increase platform differences and must exist both locally and in CI. Prefer Python tools installed through deps. When an external program is essential, document the prerequisite and check its presence explicitly.

Treat tox configuration as production code. Review changes, run the primary environment during development, and require the complete supported matrix before a release. A small matrix whose results are understood provides more evidence than many ignored or flaky environments.

Keep the declared support policy aligned with this matrix and the package metadata. Review all three when adopting a new Python release or retiring an old one. That habit makes a successful tox run evidence for a deliberate compatibility promise, rather than an isolated green check.

Record the decision in release notes as well, so users can choose a compatible interpreter before installing the package in production or automation.