Skip to main content

Tox

tox is a testing automation tool

It runs your tests against multiple Python versions simultaneously + automates linting, type-checking, and other workflows.


Why use tox?

  • 🔄 Test against Python 3.9, 3.10, 3.11, 3.12 at once
  • 🧹 Automate: tests + lint + type-check + docs
  • 📦 Libraries must support multiple Python versions (tox proves it)
  • 🏗️ Creates isolated test environments (like venv but automated)
  • 🚀 CI/CD friendly (GitHub Actions, GitLab CI, etc. use tox)

Tl;dr: If you're building a library/package → tox is non-negotiable.


When to use tox vs venv/uv

Use caseTool
Single project developmentvenv / uv
Testing multiple Python versionstox
Library/package (must support v3.9-3.12)tox
Testing + linting + type-checking pipelinetox
Web app deployment (one Python version)venv / uv

Install tox

pip install tox

Or with uv:

uv tool install tox

Check installation

tox --version

1️⃣ Configure tox: Create tox.ini

In your project root, create tox.ini:

[tox]
envlist = py39,py310,py311,py312,lint,type,docs

[testenv]
deps =
pytest
pytest-cov
requests
commands =
pytest {posargs}
pytest --cov=myapp

[testenv:lint]
deps =
flake8
black
isort
commands =
black --check .
isort --check-only .
flake8 .

[testenv:type]
deps =
mypy
types-requests
commands =
mypy myapp

[testenv:docs]
deps =
sphinx
sphinx-rtd-theme
commands =
sphinx-build -b html docs docs/_build

What this does:

  • envlist: Test on py39, py310, py311, py312, plus lint/type/docs environments
  • [testenv]: Base test environment (runs pytest)
  • [testenv:lint]: Linting environment (runs flake8, black, isort)
  • [testenv:type]: Type-checking environment (runs mypy)
  • [testenv:docs]: Documentation environment (runs sphinx)

2️⃣ Modern: Use tox.toml instead

If your project uses pyproject.toml, you can use tox.toml (modern approach):

[tool.tox]
legacy_tox_ini = "tox.ini"

[build-system]
requires = ["setuptools>=45", "wheel"]
build-backend = "setuptools.build_meta"

Or full tox config in pyproject.toml:

[tool.tox]
envlist = ["py39", "py310", "py311", "py312", "lint", "type"]

[tool.tox.testenv]
deps = ["pytest", "pytest-cov"]
commands = ["pytest"]

Note: tox.ini is more common (works everywhere). tox.toml is newer and cleaner.


3️⃣ Run all tests

tox

This runs:

  • Tests on py39, py310, py311, py312
  • Lint environment
  • Type-checking environment
  • Docs environment

Output shows which environments PASSED or FAILED.


4️⃣ Run specific environment

tox -e py311

Run only Python 3.11 tests.

tox -e lint

Run only linting environment.

tox -e py310,py311

Run Python 3.10 and 3.11.


5️⃣ Skip certain environments

tox -e py39,py310,py311,py312 --skip-missing-interpreters

Skip if Python version not installed (don't fail).

tox -e py39,py310 -x

Stop on first failure (-x flag).


6️⃣ Rebuild environments

By default, tox caches environments. Rebuild if dependencies changed:

tox -r

Or recreate specific environment:

tox -e py311 -r

7️⃣ Pass arguments to tests

tox -e py311 -- -v

Pass -v (verbose) to pytest.

tox -- tests/test_auth.py::test_login

Run specific test file.


8️⃣ List all environments

tox -l

Shows:

py39
py310
py311
py312
lint
type
docs

9️⃣ Parallel execution

tox -p

Run environments in parallel (faster).

tox -p 4

Run 4 environments at once.


🔁 One Real Project Example

Scenario: Building a Python library that needs to support Python 3.9–3.12.

Step 1: Create tox.ini

cat > tox.ini << 'EOF'
[tox]
envlist = py39,py310,py311,py312,lint

[testenv]
deps =
pytest
pytest-cov
requests
commands =
pytest tests/
pytest --cov=mylib --cov-report=term-missing

[testenv:lint]
deps =
black
isort
flake8
commands =
black --check .
isort --check-only .
flake8 .
EOF

Step 2: Install tox

pip install tox

Step 3: Run all environments

tox

Output:

py39 create: /project/.tox/py39
py39 install: pytest, pytest-cov, requests
py39 run-test-base: pytest tests/
...
PASSED: py39
PASSED: py310
PASSED: py311
PASSED: py312
PASSED: lint

✅ All environments pass = library works on Python 3.9–3.12.

Step 4: Use in CI/CD (GitHub Actions)

name: Tests

on: [push, pull_request]

jobs:
test:
runs-on: ubuntu-latest
strategy:
matrix:
python-version: ['3.9', '3.10', '3.11', '3.12']
steps:
- uses: actions/checkout@v3
- uses: actions/setup-python@v4
with:
python-version: ${{ matrix.python-version }}
- run: pip install tox
- run: tox -e py${{ matrix.python-version }}

🧠 Daily workflow to remember

# First time setup
pip install tox
cat > tox.ini << 'EOF'
[tox]
envlist = py39,py310,py311,py312,lint

[testenv]
deps = pytest
commands = pytest tests/

[testenv:lint]
deps = black,flake8
commands = black .; flake8 .
EOF

# Daily: Run tests
tox

# Specific: Test Python 3.11
tox -e py311

# Specific: Lint only
tox -e lint

# Rebuild after dependency changes
tox -r

🧠 Memory trick

"Tox = multiple test environments at once"

Think: for py in [3.9, 3.10, 3.11, 3.12]: run_tests()