How-to Guides

Practical recipes for common tox tasks. Each section answers a specific “How do I…?” question.

Quick reference

Common commands

  • Each tox subcommand has a 1 (or 2) letter shortcut, e.g. tox run = tox r, tox config = tox c.

  • Run all default environments: tox (runs everything in env_list).

  • Run a specific environment: tox run -e 3.13.

  • Run multiple environments: tox run -e lint,3.13 (sequential, in order).

  • Run environments in parallel: tox parallel -e 3.13,3.12 (see Parallel mode).

  • Run all environments matching a label: tox run -m test (see labels).

  • Run all environments matching a factor: tox run -f django (runs all envs containing the django factor).

  • Inspect configuration: tox config -e 3.13 -k pass_env.

  • Force recreation: tox run -e 3.13 -r.

Environment variables

  • View environment variables: tox c -e 3.13 -k set_env pass_env.

  • Pass through system environment variables: use pass_env.

  • Set environment variables: use set_env.

  • Setup commands: commands_pre. Teardown commands: commands_post.

  • Change working directory: change_dir (affects install commands too if using relative paths).

Logging

tox logs command invocations inside .tox/<env_name>/log. Environment variables with names containing sensitive words (access, api, auth, client, cred, key, passwd, password, private, pwd, secret, token) are logged with their values redacted to prevent accidental secret leaking in CI/CD environments.

Test with pytest

A typical pytest configuration:

env_list = ["3.13", "3.12"]

[env_run_base]
deps = ["pytest>=8"]
commands = [["pytest", { replace = "posargs", default = ["tests"], extend = true }]]
[tox]
env_list = 3.13, 3.12

[testenv]
deps = pytest>=8
commands = pytest {posargs:tests}

When running tox in parallel mode, ensure each pytest invocation is fully isolated by setting a unique temporary directory:

[env_run_base]
commands = [["pytest", "--basetemp={env_tmp_dir}", { replace = "posargs", default = ["tests"], extend = true }]]
[testenv]
commands = pytest --basetemp="{env_tmp_dir}" {posargs:tests}

Collect coverage across multiple environments

A common pattern is running tests across several Python versions and combining coverage results. Use depends to ensure coverage runs after all test environments:

env_list = ["3.13", "3.12", "coverage"]

[env_run_base]
deps = ["pytest", "coverage[toml]"]
commands = [["coverage", "run", "-p", "-m", "pytest", "tests"]]

[env.coverage]
skip_install = true
deps = ["coverage[toml]"]
depends = ["3.*"]
commands = [
    ["coverage", "combine"],
    ["coverage", "report", "--fail-under=80"],
]
[tox]
env_list = 3.13, 3.12, coverage

[testenv]
deps =
    pytest
    coverage[toml]
commands = coverage run -p -m pytest tests

[testenv:coverage]
skip_install = true
deps = coverage[toml]
depends = 3.*
commands =
    coverage combine
    coverage report --fail-under=80

The -p flag (parallel mode) creates separate .coverage.<hash> files per environment. coverage combine merges them before generating the report.

Run a one-off command (tox exec)

The tox exec subcommand runs an arbitrary command inside a tox environment without executing the configured commands, commands_pre, or commands_post. It also skips package installation. Pass the command after --:

# Open a Python shell inside the "3.13" environment
tox exec -e 3.13 -- python

# Check installed packages
tox exec -e 3.13 -- pip list

# Run a script with the environment's Python
tox exec -e 3.13 -- python scripts/migrate.py --dry-run

The command must be in the environment’s PATH or listed in allowlist_externals. tox exec is useful for debugging, running one-off scripts, or interactively exploring an environment without modifying your configuration.

Override configuration defaults

tox provides several ways to override configuration values without editing the configuration file.

User-level configuration file: tox reads a user-level config file whose location is shown in tox --help. The location can be changed via the TOX_CONFIG_FILE environment variable.

Environment variables: Any tox setting can be set via an environment variable with the TOX_ prefix:

# Use wheel packaging
TOX_PACKAGE=wheel tox run -e 3.13

CLI override: The -x (or --override) flag overrides any configuration value:

# Force editable install for a specific environment
tox run -e 3.13 -x "testenv:3.13.package=editable"

Use labels to group environments

Labels let you assign tags to environments and run them as a group with tox run -m <label>:

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

[env_run_base]
labels = ["test"]
commands = [["pytest", "tests"]]

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

[env.type]
labels = ["check"]
deps = ["mypy"]
commands = [["mypy", "src"]]
[tox]
env_list = 3.13, 3.12, lint, type

[testenv]
labels = test
commands = pytest tests

[testenv:lint]
labels = check
skip_install = true
deps = ruff
commands = ruff check .

[testenv:type]
labels = check
deps = mypy
commands = mypy src
# Run all environments labeled "check"
tox run -m check

# Run all environments labeled "test"
tox run -m test

Disallow unlisted environments

By default, running tox -e <name> with an environment name not defined in the configuration still works – tox creates an environment with default settings. This can mask typos.

For example, given:

[env.unit]
deps = ["pytest"]
commands = [["pytest"]]
[testenv:unit]
deps = pytest
commands = pytest

Running tox -e unt or tox -e unti would succeed without running any tests. An exception is made for environments that look like Python version specifiers – tox -e 3.13 or tox -e py313 would still work as intended.

Note that Python versions can be written with or without dots (py3.10 vs py310). If you define py310-lint in your configuration and accidentally run tox -e py3.10-lint, tox will detect the mismatch and suggest the correct environment name with a did you mean py310-lint? message rather than silently falling back to the base test environment.

Configure platform-specific settings

Platform-dependent commands

The current platform (sys.platform value like linux, darwin, win32) is automatically available as an implicit factor in all environments. Use platform factors to run different commands or set different dependencies per platform without encoding the platform name in the environment:

[env_list_base]
env_list = ["py313"]

[env_run_base]
deps = [
    "pytest",
    { replace = "if", condition = "factor.linux or factor.darwin", then = ["platformdirs>=3"], extend = true },
    { replace = "if", condition = "factor.win32", then = ["platformdirs>=2"], extend = true },
]
commands = [
    { replace = "if", condition = "factor.linux", then = [["python", "-c", "print('Running on Linux')"]], extend = true },
    { replace = "if", condition = "factor.darwin", then = [["python", "-c", "print('Running on macOS')"]], extend = true },
    { replace = "if", condition = "factor.win32", then = [["python", "-c", "print('Running on Windows')"]], extend = true },
    ["python", "-m", "pytest"],
]
[tox]
env_list = py313

[testenv]
deps =
    pytest
    linux,darwin: platformdirs>=3
    win32: platformdirs>=2
commands =
    linux: python -c 'print("Running on Linux")'
    darwin: python -c 'print("Running on macOS")'
    win32: python -c 'print("Running on Windows")'
    python -m pytest

This allows a single environment like py313 to adapt its behavior based on the execution platform. The platform factors work alongside regular factors from the environment name.

Common sys.platform values:

  • linux - Linux systems

  • darwin - macOS systems

  • win32 - Windows systems (both 32-bit and 64-bit)

  • cygwin - Cygwin on Windows

  • freebsd13 - FreeBSD 13.x (version varies)

  • openbsd7 - OpenBSD 7.x (version varies)

Platform factors with environment factors

Platform factors combine with regular environment factors. For example, an environment named py313-django50 has factors py313, django50, and the current platform:

env_list = [
    { product = [["py312", "py313"], ["django42", "django50"]] },
]

[env_run_base]
deps = [
    { replace = "if", condition = "factor.django42", then = ["Django>=4.2,<4.3"], extend = true },
    { replace = "if", condition = "factor.django50", then = ["Django>=5.0,<5.1"], extend = true },
    { replace = "if", condition = "factor.py312 and factor.linux", then = ["pytest-xdist"], extend = true },
    { replace = "if", condition = "factor.darwin", then = ["pyobjc-framework-Cocoa"], extend = true },
]
commands = [
    { replace = "if", condition = "factor.win32", then = [["python", "-c", "import winreg"]], extend = true },
    ["pytest"],
]
[tox]
env_list = py3{12,13}-django{42,50}

[testenv]
deps =
    django42: Django>=4.2,<4.3
    django50: Django>=5.0,<5.1
    py312,linux: pytest-xdist  # only on Python 3.12 + Linux
    darwin: pyobjc-framework-Cocoa  # only on macOS
commands =
    win32: python -c 'import winreg'  # only runs on Windows
    pytest

Negation also works with platform factors:

[env_run_base]
deps = [
    { replace = "if", condition = "not factor.win32", then = ["uvloop"], extend = true },
    { replace = "if", condition = "not factor.darwin", then = ["pyinotify"], extend = true },
]
[testenv]
deps =
    !win32: uvloop  # install uvloop on non-Windows platforms
    !darwin: pyinotify  # install pyinotify except on macOS

Platform skipping vs platform factors

There are two ways to handle platform differences:

Platform factors (recommended) - Filter individual settings per platform:

[env_run_base]
commands = [
    { replace = "if", condition = "factor.linux", then = [["pytest", "--numprocesses=auto"]], extend = true },
    { replace = "if", condition = "factor.darwin or factor.win32", then = [["pytest"]], extend = true },
]
[testenv]
commands =
    linux: pytest --numprocesses=auto
    darwin,win32: pytest

Settings without a platform factor apply to all platforms. This is ideal for most cross-platform projects.

Platform skipping - Skip entire environments when platform doesn’t match:

[env_run_base]
platform = "linux"
[testenv]
platform = linux

This skips the entire environment on non-Linux systems. Use this only when an environment genuinely cannot run on other platforms (e.g., testing Linux-specific kernel features).

Note

Platform factors are supported in both INI and TOML formats. INI uses inline syntax (linux: command), while TOML uses replace = "if" with factor.NAME conditions (see Conditional value reference).

Targeting a specific CPU architecture

Added in version 4.46.

On machines that support multiple CPU architectures (e.g. Apple Silicon running arm64 natively and x86_64 via Rosetta 2, or Linux running aarch64 and x86_64 via qemu-user), you can constrain tox environments to a specific architecture by appending the ISA name to base_python.

The architecture is derived from sysconfig.get_platform() (e.g. macosx-14.0-arm64, linux-x86_64) and normalized by virtualenv (amd64x86_64, aarch64arm64).

# Run tests on both arm64 and x86_64 interpreters
env_list = ["arm64", "x86_64"]

[env.arm64]
base_python = ["cpython3.12-64-arm64"]
commands = [["pytest"]]

[env.x86_64]
base_python = ["cpython3.12-64-x86_64"]
commands = [["pytest"]]
[tox]
env_list = arm64, x86_64

[testenv:arm64]
base_python = cpython3.12-64-arm64
commands = pytest

[testenv:x86_64]
base_python = cpython3.12-64-x86_64
commands = pytest

If the discovered interpreter’s architecture does not match the requested one, tox raises a failure — just as it does for Python version mismatches. The matched architecture is recorded in the tox journal under the machine key.

Common architecture values (after normalization):

  • x86_64 — 64-bit x86 (Intel/AMD)

  • arm64 — 64-bit ARM (Apple Silicon, Graviton, Ampere)

  • x86 — 32-bit x86

  • s390x — IBM Z mainframe

  • ppc64le — 64-bit PowerPC little-endian

Set values based on a condition

Added in version 4.40: Conditional value replacement with env.VAR lookups.

Changed in version 4.42: Added factor.NAME lookups for environment name factors and platform.

Changed in version 4.50: Added factor['NAME']/env['VAR'] subscript syntax and env_name variable.

TOML configurations can conditionally select values based on environment variables and factors using replace = "if". The condition field accepts expressions with env.VAR lookups for environment variables, factor.NAME lookups for environment name factors and platform, ==/!= comparisons, and and/or/not boolean logic.

Set a variable depending on whether you are in CI:

[env_run_base]
set_env.MATURITY = { replace = "if", condition = "env.CI", then = "release", "else" = "dev" }

Add verbose flags to commands when a DEBUG variable is set:

[env_run_base]
commands = [["pytest", { replace = "if", condition = "env.DEBUG", then = ["-vv", "--tb=long"], "else" = [], extend = true }]]

Use different dependencies based on environment factors:

[env_run_base]
deps = [
    "pytest",
    { replace = "if", condition = "factor.django50", then = ["Django>=5.0,<5.1"], "else" = ["Django>=4.2,<4.3"], extend = true },
]

Combine multiple conditions (environment variables and factors):

[env.deploy]
commands = [["deploy", { replace = "if", condition = "env.CI and env.TAG_NAME != ''", then = ["--production"], "else" = ["--dry-run"], extend = true }]]

[env_run_base]
commands = [["pytest", { replace = "if", condition = "factor.linux and not env.CI", then = ["--numprocesses=auto"], "else" = [], extend = true }]]

Use subscript syntax for version-number factors that aren’t valid Python identifiers:

[env."test-3.14"]
commands = [
    ["pytest", { replace = "if", condition = "factor['3.14']", then = ["--strict-markers"], "else" = [], extend = true }],
]

Target a specific environment by name with env_name:

[env_run_base]
set_env.EXTRA = { replace = "if", condition = "env_name == 'test-3.14'", then = "latest", "else" = "" }

For the full expression syntax and more examples, see Conditional value reference.

Handle env names that match subcommands

tox has built-in subcommands (run, list, config, etc.). If you have an environment name that matches a subcommand, use the run subcommand explicitly:

# This would be interpreted as "tox list", not "run the list environment"
# tox -e list  # does NOT work as expected

# Use the run subcommand explicitly
tox run -e list

# Or the short alias
tox r -e list

Use a custom PyPI server

By default tox uses pip to install Python dependencies. To change the index server, configure pip directly via environment variables:

[env_run_base]
set_env = { PIP_INDEX_URL = "https://my.pypi.example/simple" }

To allow the user to override the index server (e.g. for offline use), use substitution with a default:

[env_run_base]
set_env = { PIP_INDEX_URL = { replace = "env", name = "PIP_INDEX_URL", default = "https://my.pypi.example/simple" } }
[testenv]
set_env =
    PIP_INDEX_URL = https://my.pypi.example/simple

To allow the user to override the index server (e.g. for offline use), use substitution with a default:

[testenv]
set_env =
    PIP_INDEX_URL = {env:PIP_INDEX_URL:https://my.pypi.example/simple}

Use multiple PyPI servers

When not all dependencies are found on a single index, use PIP_EXTRA_INDEX_URL:

[env_run_base]
set_env.PIP_INDEX_URL = { replace = "env", name = "PIP_INDEX_URL", default = "https://primary.example/simple" }
set_env.PIP_EXTRA_INDEX_URL = { replace = "env", name = "PIP_EXTRA_INDEX_URL", default = "https://secondary.example/simple" }
[testenv]
set_env =
    PIP_INDEX_URL = {env:PIP_INDEX_URL:https://primary.example/simple}
    PIP_EXTRA_INDEX_URL = {env:PIP_EXTRA_INDEX_URL:https://secondary.example/simple}

If the index defined under PIP_INDEX_URL does not contain a package, pip will attempt to resolve it from PIP_EXTRA_INDEX_URL.

Warning

Using an extra PyPI index for installing private packages may cause security issues. If package1 is registered with the default PyPI index, pip will install package1 from the default PyPI index, not from the extra one.

Use constraint files

Constraint files define version constraints for dependencies without specifying what to install. When creating a test environment, tox invokes pip multiple times:

  1. If deps is specified, it installs those dependencies first.

  2. If the environment has a package (not package skip or skip_install true), it:

    1. Installs the package dependencies.

    2. Installs the package itself.

When constrain_package_deps = true is set, {env_dir}/constraints.txt is generated during install_deps based on the specifications in deps. These constraints are then passed to pip during install_package_deps, raising an error when package dependencies conflict with test dependencies.

For stronger guarantees, set use_frozen_constraints = true to generate constraints from the exact installed versions (via pip freeze). This catches incompatibilities with any previously installed dependency.

Note

Constraint files are a subset of requirement files. You can pass a constraint file wherever a requirement file is accepted.

Use extras

If your package defines optional dependency groups (extras) in pyproject.toml, you can install them in tox environments via the extras configuration:

# pyproject.toml
[project.optional-dependencies]
testing = ["pytest>=8", "coverage"]
docs = ["sphinx>=7"]
# tox.toml
[env_run_base]
extras = ["testing"]

[env.docs]
extras = ["docs"]
commands = [["sphinx-build", "-W", "docs", "docs/_build/html"]]
[testenv]
extras = testing

[testenv:docs]
extras = docs
commands = sphinx-build -W docs docs/_build/html

This installs your package together with the specified extras, avoiding the need to duplicate dependency lists in both pyproject.toml and your tox configuration.

Install extras without the package

Sometimes you need the package’s dependencies (including extras) without installing the package itself. For example, coverage combining, documentation builds, or linting environments that share the same dependency set. Use package = "deps-only" instead of skip_install = true combined with manually duplicated deps:

# pyproject.toml
[project]
name = "myproject"
dependencies = ["httpx>=0.27"]

[project.optional-dependencies]
docs = ["sphinx>=7", "furo"]
# tox.toml
[env.docs]
package = "deps-only"
extras = ["docs"]
commands = [["sphinx-build", "-W", "docs", "docs/_build/html"]]
[testenv:docs]
package = deps-only
extras = docs
commands = sphinx-build -W docs docs/_build/html

This reads your pyproject.toml directly (no build step) and installs httpx, sphinx, and furo into the environment. If your dependencies are dynamic, tox falls back to using the packaging environment to extract metadata.

Install locked dependencies from pylock.toml

If your project maintains PEP 751 lock files (pylock.toml), you can install those locked dependencies directly via the pylock configuration. The pylock setting is mutually exclusive with deps — use one or the other. Each package in the lock file is installed as a pinned requirement (name==version) with --no-deps (since the lock file already contains all transitive dependencies), and tox automatically recreates the environment when the lock file changes.

# tox.toml
[env_run_base]
pylock = "pylock.toml"
[testenv]
pylock = pylock.toml

The locked dependencies are installed first, then the project itself is built and installed normally (unless skip_install or package = "skip" is set). When the lock file contains PEP 751 extras or dependency groups, use the existing extras and dependency_groups settings to select which ones to include. Packages with markers like 'docs' in extras or 'dev' in dependency_groups are filtered at install time — only packages matching the selected extras/groups (and the target Python’s platform markers) are installed:

[env.docs]
pylock = "pylock.toml"
extras = ["docs"]

[env.dev]
pylock = "pylock.toml"
dependency_groups = ["dev"]
[testenv:docs]
pylock = pylock.toml
extras = docs

[testenv:dev]
pylock = pylock.toml
dependency_groups = dev

Customize virtualenv creation

tox uses virtualenv to create Python virtual environments. Customize virtualenv behavior through environment variables:

[env_run_base]
set_env.VIRTUALENV_PIP = "22.1"
set_env.VIRTUALENV_SYSTEM_SITE_PACKAGES = "true"
[testenv]
set_env =
    VIRTUALENV_PIP = 22.1
    VIRTUALENV_SYSTEM_SITE_PACKAGES = true

Any CLI flag for virtualenv can be set as an environment variable with the VIRTUALENV_ prefix (in uppercase). Consult the virtualenv documentation for supported values.

Clean external caches during recreation

Tools like pre-commit maintain their own caches outside the tox environment directory. When you recreate an environment with tox run -r, those external caches are left behind. Use recreate_commands to run cleanup commands inside the old environment before it is removed:

[env_run_base]
deps = ["pre-commit"]
recreate_commands = [["{env_python}", "-Im", "pre_commit", "clean"]]
commands = [["pre-commit", "run", "--all-files"]]
[testenv]
deps = pre-commit
recreate_commands = {env_python} -Im pre_commit clean
commands = pre-commit run --all-files

These commands only run during recreation – they are skipped on first creation and on normal re-runs. Failures are logged as warnings and never block the recreation itself.

Test across old and new Python versions

When a project must support both very old (e.g. Python 3.6) and very new (e.g. Python 3.15) interpreters, no single virtualenv release covers both. Use virtualenv_spec to pin a different virtualenv version per environment:

env_list = ["3.6", "3.15", "3.13"]

[env_run_base]
deps = ["pytest"]
commands = [["pytest"]]

[env."3.6"]
virtualenv_spec = "virtualenv<20.22.0"
[tox]
env_list = 3.6, 3.15, 3.13

[testenv]
deps = pytest
commands = pytest

[testenv:3.6]
virtualenv_spec = virtualenv<20.22.0

The 3.6 environment uses an older virtualenv that still supports Python 3.6, while other environments use the default (imported) virtualenv. The first run bootstraps the pinned version; subsequent runs reuse the cached bootstrap.

Generate environment matrices in TOML

Use the product dict in env_list to generate environments from the Cartesian product of factor groups. Each factor group is an array of strings or a range dict. Combinations are joined with -:

env_list = [
    "lint",
    { product = [
        { prefix = "py3", start = 12, stop = 14 },
        ["django42", "django50"],
    ] },
]

[env_run_base]
package = "skip"
deps = [
    "pytest",
    { replace = "if", condition = "factor.django42", then = ["Django>=4.2,<4.3"], extend = true },
    { replace = "if", condition = "factor.django50", then = ["Django>=5.0,<5.1"], extend = true },
]
commands = [["pytest"]]
[tox]
env_list = py3{12-14}-django{42,50}

[testenv]
package = skip
deps =
    pytest
    django42: Django>=4.2,<4.3
    django50: Django>=5.0,<5.1
commands = pytest

This generates lint, py312-django42, py312-django50, py313-django42, py313-django50, py314-django42, py314-django50.

To skip incompatible combinations, add exclude – this is only available in TOML:

env_list = [
    { product = [["py312", "py313"], ["django42", "django50"]], exclude = ["py312-django50"] },
]

Test a matrix of configurations with env_base

When multiple environments share the same deps, commands, and other settings but differ only by factors, use env_base templates instead of repeating configuration across [env.X] sections. The factors key defines the Cartesian product of factor groups, and each generated environment inherits all other settings from the template:

[env_base.django]
factors = [
    { prefix = "py3", start = 13, stop = 14 },
    ["django42", "django50"],
]
package = "skip"
deps = [
    "pytest",
    { replace = "if", condition = "factor.django42", then = ["Django>=4.2,<4.3"], extend = true },
    { replace = "if", condition = "factor.django50", then = ["Django>=5.0,<5.1"], extend = true },
]
commands = [["pytest"]]

This generates django-py313-django42, django-py313-django50, django-py314-django42, django-py314-django50. Each environment resolves factor conditions independently – django-py313-django42 gets Django>=4.2,<4.3 while django-py314-django50 gets Django>=5.0,<5.1.

To override a specific generated environment, add an explicit [env.NAME] section:

[env.django-py314-django50]
description = "bleeding edge"

The inheritance chain is: [env.{name}] > [env_base.{template}] > [env_run_base].

See Environment base templates for the full reference.

Ignore command exit codes

When multiple commands are defined in commands, tox runs them sequentially and stops at the first failure (non-zero exit code). To ignore the exit code of a specific command, prefix it with -:

[env_run_base]
commands = [
    ["-", "python", "-c", "import sys; sys.exit(1)"],
    ["python", "--version"],
]
[testenv]
commands =
    - python -c 'import sys; sys.exit(1)'
    python --version

To invert the exit code (fail if the command returns 0, succeed otherwise), use the ! prefix:

[env_run_base]
commands = [
    ["!", "python", "-c", "import sys; sys.exit(1)"],
    ["python", "--version"],
]
[testenv]
commands =
    ! python -c 'import sys; sys.exit(1)'
    python --version

Retry flaky commands

Commands that fail due to transient errors (network timeouts, flaky tests) can be automatically retried using commands_retry. The value specifies how many times to retry a failed command – a value of 2 means each command is attempted up to 3 times total. Retries apply to commands_pre, commands, and commands_post. Commands prefixed with - (ignore exit code) are never retried.

[env.integration]
description = "run integration tests with retries for flaky network calls"
commands_retry = 2
commands = [["pytest", "tests/integration"]]
[testenv:integration]
description = run integration tests with retries for flaky network calls
commands_retry = 2
commands = pytest tests/integration

Control color output

tox uses colored output by default. To disable it, use any of these methods:

# Via environment variable
NO_COLOR=1 tox run

# Via TERM
TERM=dumb tox run

# Via CLI flag
tox run --colored no

Use tox in CI/CD pipelines

tox works well in continuous integration systems. We recommend installing tox via uv for significantly faster setup times. Adding tox-uv also replaces pip with uv inside tox environments, speeding up dependency installation.

GitHub Actions:

# .github/workflows/tests.yml
name: tests
on: [push, pull_request]
jobs:
  test:
    runs-on: ubuntu-latest
    strategy:
      matrix:
        python-version: ["3.12", "3.13", "3.14"]
    steps:
      - uses: actions/checkout@v4
      - uses: actions/setup-python@v5
        with:
          python-version: ${{ matrix.python-version }}
      - uses: astral-sh/setup-uv@v5
      - run: uv tool install tox --with tox-uv
      - run: tox run -e ${{ matrix.python-version }}

GitLab CI:

# .gitlab-ci.yml
test:
  image: python:3.13
  before_script:
    - curl -LsSf https://astral.sh/uv/install.sh | sh
    - uv tool install tox --with tox-uv
  script:
    - tox run -e 3.13

Run tox inside a Docker container

Build a lightweight Docker image that contains tox and your target Python versions. Using uv keeps the image small and installation fast:

FROM python:3.13-slim

# Install build tools commonly needed by C-extension packages
RUN set -eux; \
    apt-get update; \
    apt-get install -y --no-install-recommends gcc make; \
    rm -rf /var/lib/apt/lists/*

# Install tox (with tox-uv for faster dependency resolution)
COPY --from=ghcr.io/astral-sh/uv:latest /uv /usr/local/bin/uv
RUN uv tool install tox --with tox-uv

ENV PATH="/root/.local/bin:$PATH"
WORKDIR /app

Mount your project directory and run tox:

docker build -t tox-runner .
docker run --rm -v "$(pwd)":/app tox-runner tox run -e 3.13

To test against multiple Python versions in the same image, start from a base image and add the versions you need:

FROM python:3.13-slim

RUN set -eux; \
    apt-get update; \
    apt-get install -y --no-install-recommends gcc make; \
    rm -rf /var/lib/apt/lists/*

COPY --from=ghcr.io/astral-sh/uv:latest /uv /usr/local/bin/uv

# Install additional Python versions via uv
RUN uv python install 3.12 3.11

RUN uv tool install tox --with tox-uv
ENV PATH="/root/.local/bin:$PATH"
WORKDIR /app
docker run --rm -v "$(pwd)":/app tox-runner tox run -e 3.13,3.12,3.11

Note

The previously recommended 31z4/tox Docker image has been archived and is no longer maintained. The image is still available on Docker Hub but may not receive updates. Building your own image as shown above is the recommended approach.

Build documentation with Sphinx

Orchestrate Sphinx documentation builds with tox to integrate them into CI:

[env.docs]
description = "build documentation"
deps = ["sphinx>=7"]
commands = [
    ["sphinx-build", "-d", "{env_tmp_dir}/doctree", "docs", "{work_dir}/docs_out", "--color", "-b", "html"],
    ["python", "-c", "print(f'documentation available under file://{work_dir}/docs_out/index.html')"],
]
[testenv:docs]
description = build documentation
deps =
    sphinx>=7
commands =
    sphinx-build -d "{envtmpdir}{/}doctree" docs "{toxworkdir}{/}docs_out" --color -b html
    python -c 'print(r"documentation available under file://{toxworkdir}{/}docs_out{/}index.html")'

This approach avoids the platform-specific Makefile generated by Sphinx and works cross-platform.

Build documentation with mkdocs

Define separate environments for developing and deploying mkdocs documentation:

[env.docs]
description = "run a development server for documentation"
deps = [
    "mkdocs>=1.3",
    "mkdocs-material",
]
commands = [
    ["mkdocs", "build", "--clean"],
    ["python", "-c", "print('###### Starting local server. Press Control+C to stop ######')"],
    ["mkdocs", "serve", "-a", "localhost:8080"],
]

[env.docs-deploy]
description = "build and deploy documentation"
deps = [
    "mkdocs>=1.3",
    "mkdocs-material",
]
commands = [["mkdocs", "gh-deploy", "--clean"]]
[testenv:docs]
description = Run a development server for working on documentation
deps =
    mkdocs>=1.3
    mkdocs-material
commands =
    mkdocs build --clean
    python -c 'print("###### Starting local server. Press Control+C to stop server ######")'
    mkdocs serve -a localhost:8080

[testenv:docs-deploy]
description = built fresh docs and deploy them
deps = {[testenv:docs]deps}
commands = mkdocs gh-deploy --clean

Debug a failing tox environment

When an environment fails, use these techniques to investigate:

  1. Increase verbosity to see detailed command output:

    tox run -e 3.13 -vv
    
  2. Inspect resolved configuration to verify settings are what you expect:

    # Show all configuration for an environment
    tox config -e 3.13
    
    # Show specific keys
    tox config -e 3.13 -k deps commands pass_env set_env
    
  3. Check log files in .tox/<env_name>/log/ for full command output with timestamps.

  4. Run a command interactively inside the environment:

    tox exec -e 3.13 -- python
    tox exec -e 3.13 -- pip list
    
  5. Force recreation if you suspect a stale environment:

    tox run -e 3.13 -r
    
  6. Check for misplaced config keys. Options in the wrong section are silently ignored. Use -v to surface warnings about unrecognized keys, or run tox config to see # !!! unused: markers:

    tox run -v
    tox config -e 3.13
    
  7. Extract configuration in different formats. Use --format to choose between ini (default), json, or toml. Write to a file with -o instead of stdout:

    tox config -e py -k deps commands                          # INI (default)
    tox config -e py -k deps commands --format json             # JSON to stdout
    tox config -e py -k deps commands --format toml -o out.toml # TOML to file
    

    Given a tox.ini with deps = pytest and commands = pytest {posargs}, the output looks like:

    [testenv:py]
    deps = pytest
    commands = pytest
    
    {
      "env": {
        "py": {
          "deps": [
            "pytest"
          ],
          "commands": [
            "pytest"
          ]
        }
      }
    }
    
    [env.py]
    deps = ["pytest"]
    commands = ["pytest"]
    

    The JSON and TOML formats preserve native types (booleans, integers, floats, arrays, dicts) and use the same key structure as tox.toml (env.<name> for environments, tox for core settings). The -o flag always writes without color codes.

    To get the list of environments programmatically, query -k type — the env keys in the output are the environment names:

    tox config -k type --format json | python -c "import json,sys; print(*json.load(sys.stdin)['env'])"
    

Reuse an environment without network

When working offline, on a plane, or in an air-gapped CI environment, tox still attempts to install dependencies and the project package on every run. If the environment was previously set up and nothing has changed, you can skip all installation steps with --skip-env-install:

# First run: installs everything normally
tox run -e 3.13

# Subsequent runs: skip all installation, reuse existing environment
tox run -e 3.13 --skip-env-install

This skips:

  • Installing deps and dependency groups

  • Building and installing the project package

The environment must already exist from a previous run. Commands (commands_pre, commands, commands_post) still execute normally.

--skip-env-install differs from --skip-pkg-install in scope: --skip-pkg-install only skips the package build and install step, while --skip-env-install additionally skips dependency installation. Use --skip-pkg-install when you want to refresh dependencies but not rebuild the package. Use --skip-env-install when you want to skip all installation entirely.

# Skip only package build/install, still install deps
tox run -e 3.13 --skip-pkg-install

# Skip everything: deps + package
tox run -e 3.13 --skip-env-install

Run interactive programs

Interactive programs like Python REPL, debuggers, or TUI applications need direct terminal access to handle user input and query console properties. By default, tox pipes stdout/stderr to capture output for logging, which breaks terminal APIs that require real console handles.

Use --no-capture (or -i) to disable output capture and give the subprocess direct access to the terminal:

# Open a Python REPL with full terminal support
tox run -e 3.13 -i -- python

# Run a debugger interactively
tox run -e 3.13 -i -- python -m pdb script.py

# Use a TUI application
tox run -e 3.13 -i -- pytest --pdb

The --no-capture flag is mutually exclusive with --result-json (which requires output capture) and parallel mode (where multiple environments’ output would interleave). When enabled, tox cannot log command output to .tox/<env_name>/log/ files.

Note

tox exec always runs in interactive mode without output capture. Use tox exec for one-off commands that don’t need the full environment setup (see Run a one-off command (tox exec)). Use tox run --no-capture when you need to run the configured commands interactively.

Access full logs

tox logs command invocations inside .tox/<env_name>/log. Each execution is recorded in a file named <index>-<run_name>.log, containing the command, environment variables, working directory, exit code, and output.

Environment variables with names containing sensitive words (access, api, auth, client, cred, key, passwd, password, private, pwd, secret, token) are logged with their values redacted to prevent accidental secret leaking.

Understand InvocationError exit codes

When a command executed by tox fails, an InvocationError is raised:

ERROR: InvocationError for command
       '<command defined in tox config>' (exited with code 1)

Always check the documentation for the command that failed. For example, for pytest, see the pytest exit codes.

On Unix systems, exit codes larger than 128 indicate a fatal signal. tox provides a hint in these cases:

ERROR: InvocationError for command
       '<command>' (exited with code 139)
Note: this might indicate a fatal error signal (139 - 128 = 11: SIGSEGV)

Signal numbers are documented in the signal man page.

Pin a default Python version

When environments like lint or type don’t contain a Python factor, tox uses the Python it’s installed into. This varies across machines – a contributor on Ubuntu 22.04 gets Python 3.10, while Fedora 37 gives 3.11 – leading to unreproducible results or failures when dependencies don’t support the host’s Python version.

Set default_base_python to pin a fallback interpreter for all environments without a Python factor:

[env_run_base]
default_base_python = ["3.14", "3.13"]
[testenv]
default_base_python = 3.14, 3.13

Environments with a Python factor (e.g. 3.13, py313) or an explicit base_python setting are unaffected.

Future-proof env_list with open-ended ranges

Instead of updating env_list every time a new Python version is released, use open-ended ranges:

env_list = [
    { product = [{ prefix = "py3", start = 10 }] },
    "lint",
]
[tox]
env_list = py3{10-}, lint

This expands up to the latest supported CPython version known to tox. When you upgrade tox after a new Python release, the range automatically includes the new version.

To start from the oldest supported version:

env_list = [
    { product = [{ prefix = "py3", stop = 13 }] },
    "lint",
]
[tox]
env_list = py3{-13}, lint

This expands down from the oldest supported CPython version. Both forms can be mixed with explicit values:

env_list = [
    { product = [{ prefix = "py3", start = 10 }] },
    "py38",
    "lint",
]
[tox]
env_list = py3{10-, 8}, lint

See Generative environment list for the full range syntax reference.

Test end-of-life Python versions

tox uses virtualenv under the hood. Newer virtualenv versions drop support for older Python interpreters:

To test against these versions, pin virtualenv:

requires = ["virtualenv<20.22.0"]
[tox]
requires = virtualenv<20.22.0

Use tox with different build backends

tox works with any PEP 517/PEP 518 compliant build backend. Configure the backend in pyproject.toml:

Hatchling (hatch):

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

Flit:

[build-system]
requires = ["flit_core>=3.4"]
build-backend = "flit_core.buildapi"

PDM:

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

tox automatically detects and uses whatever backend is specified in [build-system]. No additional tox configuration is needed. For build backends that need extra configuration during the build, use config_settings_build_wheel and related options.

Reference the built package path in commands

When tox builds an sdist or wheel for your project it stores the path in the TOX_PACKAGE environment variable (see Injected environment variables). Reference it in commands (or any other config value) to run post-build checks such as twine check, check-wheel-contents, or pkginfo against the exact artifact that was just built and installed.

Use the explicit environment variable reference (see pyproject.toml - native):

[env.check]
description = "check the built package"
deps = ["twine"]
commands = [["twine", "check", { replace = "env", name = "TOX_PACKAGE" }]]
[testenv:check]
description = check the built package
deps = twine
commands = twine check {env:TOX_PACKAGE}

TOX_PACKAGE is only set in run environments where a package has been built. If there are multiple artifacts (for example both an sdist and a wheel), the paths are joined with os.pathsep.

Tip

If you need a glob-based approach instead (e.g. matching files produced outside of tox), use the {glob:PATTERN} substitution — see Substitution quick reference.

Migrate from tox.ini to tox.toml

TOML is the recommended configuration format for new projects. Here is how common INI patterns translate to TOML:

Basic structure:

# tox.toml - values at root level are core settings
requires = ["tox>=4.20"]
env_list = ["3.13", "3.12", "lint"]

# base settings for run environments
[env_run_base]
deps = ["pytest>=8"]
commands = [["pytest", "tests"]]

# environment-specific overrides
[env.lint]
skip_install = true
deps = ["ruff"]
commands = [["ruff", "check", "."]]
[tox]
requires = tox>=4.20
env_list = 3.13, 3.12, lint

[testenv]
deps = pytest>=8
commands = pytest tests

[testenv:lint]
skip_install = true
deps = ruff
commands = ruff check .

Key differences:

  • Strings must be quoted in TOML: description = "run tests" vs description = run tests

  • Lists use JSON syntax: deps = ["pytest", "ruff"] vs multi-line deps = \n pytest \n ruff

  • Commands are list-of-lists: commands = [["pytest", "tests"]] vs commands = pytest tests

  • Positional arguments use replacement objects: { replace = "posargs", default = ["tests"] } vs {posargs:tests}

  • Environment variables in set_env use { replace = "env", name = "VAR" } vs {env:VAR}

  • Section references use { replace = "ref", ... } vs {[section]key}

  • Factor conditions use { replace = "if", condition = "factor.NAME", ... } vs NAME:

  • Generative environment lists use { product = [...] } dicts vs {a,b}-{c,d} brace expansion

Format your tox configuration files

Consistent formatting makes configuration files easier to read and review. The tox-dev organization maintains opinionated formatters for both TOML and INI configurations, available as pre-commit hooks or standalone CLI tools.

Use tox-toml-fmt for tox.toml or TOML-based configuration in pyproject.toml. It standardizes quoting, array formatting, and table organization:

# .pre-commit-config.yaml
- repo: https://github.com/tox-dev/toml-fmt
  rev: "1.6.0"
  hooks:
    - id: tox-toml-fmt

Also available as a standalone command via pipx install tox-toml-fmt.

Use tox-ini-fmt for tox.ini files. It normalizes boolean fields, orders sections consistently, and formats multi-line values with uniform indentation:

# .pre-commit-config.yaml
- repo: https://github.com/tox-dev/tox-ini-fmt
  rev: "1.7.1"
  hooks:
    - id: tox-ini-fmt

Also available as a standalone command via pipx install tox-ini-fmt.