The go-to resource for upgrading Python, Django, Flask, and your dependencies.

tox.ini Configuration for Testing Python Packages Across 3.10‑3.13


When we need to test a Python package across multiple versions—say py3.10 through py3.13—we face a choice. One approach involves manual virtual environments for each interpreter, repeating dependency installs each time. CI platforms offer matrix jobs, but configuring them often leads to duplication. Tox addresses this by creating isolated environments automatically from a single configuration file.

We can think of tox like early tabulating machines for censuses: it doesn’t invent new capabilities, but organizes repetitive work efficiently. Here’s a tox.ini that tests py310-py313 with pytest, coverage, mypy, and ruff.

Installation & prerequisites

bash\n$ pip install tox tox-gh-actions # tox + GitHub Actions plugin\n$ # Or with Poetry:\n$ poetry add --group dev tox\n$ # Or uv:\n$ uv add --dev tox\n

# Verify installation
tox --version
# list environments
tox --listenvs | grep py

Minimal tox.ini: pytest across Python versions\n\nLet’s start with the essentials. The [tox] section defines our environment list—py310 through py313—and skips any missing interpreters (handy if py3.13 isn’t installed locally).\n\nThe [testenv] section applies to all these environments: it installs pytest and runs it on tests/.\n\nini\n[tox]\nenvlist = py310,py311,py312,py313\nskip_missing_interpreters = true\n\n[testenv]\ndeps = pytest>=8.0 ; Common test dependencies\ncommands = pytest tests/ -v\n\n\nRun the full suite:\n\nbash\ntox\n\n\nYou’ll see output like:\n\nbash\n$ tox\ngpy310:create PASSED\npy310: commands succeeded\npy311:create PASSED\n... (abbreviated)\ncongratulations :) All tests passed on all platforms!\n\n\nEach env builds once and caches; subsequent runs reuse them.

Run a single version:

tox -e py312

Run in parallel (tox 4+):

tox -p all

Factored environments: adding coverage, mypy, ruff\n\nTo run multiple tools per Python version, tox supports factored environments. envlist = py{310,311,312,313}-{pytest,coverage,mypy,ruff} creates py310-pytest, py310-coverage, etc.—24 envs total, but only active ones run.\n\nThe base [testenv] installs common deps. Specialized sections override commands:\n\nini\n[tox]\nenvlist = py{310,311,312,313}-{pytest,coverage,mypy,ruff}\nskip_missing_interpreters = true\n\n[testenv]\n deps =\n pytest>=8.0 ; Unit testing\n pytest-cov ; Coverage\n mypy ; Type checking\n ruff>=0.5.0 ; Linting/formatting\n commands = pytest {posargs:tests/} ; Default: run tests\n\n[testenv:coverage]\ncommands =\n coverage run -m pytest tests/\n coverage xml -o coverage.{envname}.xml\n coverage html -o htmlcov.{envname}\n coverage report --fail-under=90\n\n[testenv:mypy]\ncommands = mypy src/ --strict\n\n[testenv:ruff]\ncommands = ruff check . --fix\n\n\n{posargs:tests/} passes extra args like tox -e py312-pytest tests/unit/test_foo.py.\n\nThis scales but can be verbose; pyproject.toml consolidates deps/commands.

Run a single matrix entry:

tox -e py312-coverage

Run the full matrix:

tox -e ALL

pyproject.toml integration (tox 4+)

[build-system]
requires = ["hatchling"]
build-backend = "hatchling.build"

[tool.tox]
legacy_toxfile = false
envlist = py{310-313}

[tool.tox.testenv]
deps = [
    "pytest>=8.0",
    "pytest-cov",
    "mypy",
    "ruff>=0.5.0",
]
commands = [
    "pytest {posargs:tests/}",
    "coverage run --append -m pytest tests/",
    "mypy src/",
    "ruff check .",
]

[tool.tox.gh]
envlist = py313

tox --no-provision speeds up CI runs.

Performance notes (approximate, M2 Mac, ~500-test suite)\n\nOn my M2 Mac, pytest alone across py310-py313 took around 150 seconds serially; adding coverage, mypy, and ruff pushed it to 220 seconds. With tox -p all (tox 4+ parallel), the full suite dropped to 55 seconds—though results vary by hardware, test complexity, and caching.

CI/CD – GitHub Actions

name: Test
on: [push, pull_request]
jobs:
  test:
    runs-on: ubuntu-latest
    steps:
      - uses: actions/checkout@v4
      - uses: actions/setup-python@v5
        with:
          python-version: '3.13'
      - run: pipx install tox tox‑gh‑actions poetry
      - run: tox

tox‑gh‑actions automatically creates the matrix for the supported Python versions.

Common issues and workarounds\n\nTox handles most cases gracefully, though:\n\n1. Missing interpreters: skip_missing_interpreters = true skips them automatically—no manual intervention.\n2. Slow first runs: Provisioning downloads Pythons/tools; use tox --no-provision on CI/repeat runs to reuse venvs.\n3. Dep conflicts: Factor with ; python_version >= '3.12': or pins, e.g. deps = pytest; python_version < '3.12': \"typing_extensions\".\n4. Aggregating coverage: tox runs per-env; post-combine with coverage combine .tox/py*/.coverage/ && coverage html.\n5. Experimental Pythons (e.g., free-threaded py3.13): Append py313t; test selectively.\n\nYou’ll spot these in logs—tox -vv for verbose debugging.

When tox might not fit—and alternatives\n\nTox shines for multi-Python testing, but consider alternatives:\n\n- Single-version projects: Plain pytest or uv test suffices—no need for env management.\n- Speed-focused: nox uses Python scripts for flexibility; uv is faster for modern workflows.\n- Parallel within version: pytest-xdist parallelizes tests per Python, complementing or replacing tox.\n- Docker CI: Native matrices avoid tox provisioning overhead.\n- Hatch/POOQR: If using Hatch for packaging, its built-in testing integrates seamlessly.\n\nChoose based on your stack—tox pairs well with traditional pyproject setups, while nox/uv suit scriptable or ultra-fast needs.

Putting it together\n\nWe’ll start with a minimal tox.ini for pytest, then layer in coverage, mypy, and ruff. You can copy these configurations directly—though adapt deps and paths to your project. For pyproject.toml integration (tox 4+), see the example above.\n\nOf course, tox works best for projects needing broad Python compatibility; for single-version or Docker-heavy CI, other tools may suit better.\n\nRelated: pytest + tox + coverage, pytest-xdist

Sponsored by Durable Programming

Need help maintaining or upgrading your Python application? Durable Programming specializes in keeping Python apps secure, performant, and up-to-date.

Hire Durable Programming