Plugin How-to Guides

Plugin hook lifecycle

The diagram below shows when each plugin hook fires during a tox run:

        flowchart TD
    start(( )) --> register[tox_register_tox_env]
    register --> add_option[tox_add_option]
    add_option --> extend[tox_extend_envs]
    extend --> core_config[tox_add_core_config]
    core_config --> env_config[tox_add_env_config]

    env_config --> envloop

    subgraph envloop [for each environment]
        direction TB
        on_install[tox_on_install]
        on_install --> before[tox_before_run_commands]
        before --> cmds[run commands]
        cmds --> after[tox_after_run_commands]
        after --> teardown[tox_env_teardown]
    end

    teardown --> done(( ))

    classDef setupStyle fill:#dbeafe,stroke:#3b82f6,stroke-width:2px,color:#1e3a5f
    classDef envStyle fill:#dcfce7,stroke:#22c55e,stroke-width:2px,color:#14532d
    classDef cmdStyle fill:#ede9fe,stroke:#8b5cf6,stroke-width:2px,color:#3b0764
    classDef installStyle fill:#ffedd5,stroke:#f97316,stroke-width:2px,color:#7c2d12

    class register,add_option,extend,core_config,env_config setupStyle
    class before,after,teardown envStyle
    class cmds cmdStyle
    class on_install installStyle
    

Add custom config to an environment

Use tox_add_env_config to register new configuration keys on every tox environment. Users can then set these keys in tox.toml or tox.ini like any built-in setting.

from tox.config.sets import EnvConfigSet
from tox.plugin import impl
from tox.session.state import State


@impl
def tox_add_env_config(env_conf: EnvConfigSet, state: State) -> None:
    env_conf.add_config(
        keys=["my_timeout"],
        of_type=int,
        default=30,
        desc="timeout in seconds for custom checks",
    )

Access the value later via env_conf["my_timeout"].

Run code before or after commands

Use tox_before_run_commands and tox_after_run_commands to execute logic around the commands phase. The tox_after_run_commands hook receives the exit code and the list of Outcome objects for each command.

import time

from tox.execute import Outcome
from tox.plugin import impl
from tox.tox_env.api import ToxEnv

_start: float = 0


@impl
def tox_before_run_commands(tox_env: ToxEnv) -> None:
    global _start  # noqa: PLW0603
    _start = time.monotonic()


@impl
def tox_after_run_commands(
    tox_env: ToxEnv, exit_code: int, outcomes: list[Outcome]
) -> None:
    elapsed = time.monotonic() - _start
    tox_env.conf.core["report"].verbosity0(
        f"{tox_env.conf['env_name']} finished in {elapsed:.1f}s "
        f"(exit code {exit_code})",
    )

Intercept package installations

Use tox_on_install to run logic whenever tox installs packages (deps, the project itself, etc.). The section and of_type parameters identify which install phase triggered the call.

from typing import Any

from tox.plugin import impl
from tox.tox_env.api import ToxEnv


@impl
def tox_on_install(tox_env: ToxEnv, arguments: Any, section: str, of_type: str) -> None:
    print(f"Installing [{section}] {of_type}: {arguments}")  # noqa: T201

Clean up after an environment

Use tox_env_teardown to run cleanup logic after an environment finishes, regardless of whether commands succeeded or failed.

from tox.plugin import impl
from tox.tox_env.api import ToxEnv


@impl
def tox_env_teardown(tox_env: ToxEnv) -> None:
    cache_dir = tox_env.env_dir / ".cache"
    if cache_dir.exists():
        import shutil

        shutil.rmtree(cache_dir)

Register a custom environment runner

Use tox_register_tox_env to register a custom run or package environment type. This is the hook used by plugins like tox-uv to replace the default virtualenv-based runner.

from tox.plugin import impl
from tox.tox_env.python.virtual_env.runner import VirtualEnvRunner
from tox.tox_env.register import ToxEnvRegister


class MyRunner(VirtualEnvRunner):
    @staticmethod
    def id() -> str:
        return "my-runner"

    # override methods to customize behavior


@impl
def tox_register_tox_env(register: ToxEnvRegister) -> None:
    register.add_run_env(MyRunner)

Set runner = my-runner in a tox environment to use it.

Package a plugin for distribution

While toxfile.py works for project-local plugins, distributable plugins are standard Python packages that declare the tox entry point.

In pyproject.toml:

[project]
name = "tox-myplugin"
version = "1.0.0"
dependencies = ["tox>=4"]

[project.entry-points.tox]
myplugin = "tox_myplugin"

In src/tox_myplugin/__init__.py, define your hooks exactly as in toxfile.py:

from tox.plugin import impl


@impl
def tox_append_version_info() -> str:
    return "myplugin-1.0.0"

After pip install tox-myplugin, tox discovers the plugin automatically via the entry point.

Logging

tox uses Python’s standard logging module. Plugin authors can use it to emit messages that integrate with tox’s output. The root logger is configured by tox at startup, so any logging call from plugin code is automatically handled.

Getting a logger

Use logging.getLogger() with your plugin’s package name:

import logging

logger = logging.getLogger(__name__)

You can also call module-level functions like logging.info(...) directly, but using a named logger makes it easier for users to filter output.

Verbosity levels

tox maps its -v / -q flags to standard logging levels:

Flags

Logging level

Notes

-q (quiet)

CRITICAL

Almost nothing is shown.

(default)

ERROR

Only errors are shown.

-v

WARNING

Warnings and above are shown. This is the first level that appears in normal output.

-vv

INFO

Informational messages appear.

-vvv

DEBUG

Debug messages appear, along with timestamps and source file locations.

Tip

For messages that should be visible during normal usage (e.g. tox -v), use logging.warning(). Use logging.info() for detail that is only useful when diagnosing issues.

Coloring

tox applies color automatically based on the log level (when color output is enabled):

  • ERROR and above: red

  • WARNING: cyan

  • INFO and below: white

There is a special convention for the WARNING level: if the message format string is "%s%s> %s" the second argument is rendered in a dimmed style and the > acts as a separator. This is the pattern tox itself uses to show the tox-environment prefix:

# The second argument (plugin name) appears dimmed, followed by "> message"
logging.warning("%s%s> %s", "", "my-plugin", "This is a warning message")

For other levels (including ERROR), the entire line is colored, so inline formatting is not possible.

Note

Color is controlled by the --colored flag and the NO_COLOR / FORCE_COLOR environment variables. Plugin code does not need to handle color escapes manually.

Debug output

At -vvv (DEBUG level), tox adds extra metadata to every log line:

  • elapsed time in milliseconds

  • one-letter level indicator (D, I, W, E, C)

  • source file and line number

This is useful for profiling and tracing plugin behavior.

Complete example

import logging

from tox.plugin import impl
from tox.tox_env.api import ToxEnv

logger = logging.getLogger(__name__)


@impl
def tox_before_run_commands(tox_env: ToxEnv) -> None:
    # Visible at -vv or higher
    logger.info("preparing environment %s", tox_env.name)
    # Visible at -v or higher, with dimmed plugin name prefix
    logger.warning(
        "%s%s> %s", "", __package__, f"running pre-checks for {tox_env.name}"
    )

Extension points

Plugin management for tox using pluggy.

Pluggy discovers a plugin by looking up for entry-points named tox, for example in a pyproject.toml:

[project.entry-points.tox]
your_plugin = "your_plugin.hooks"

Therefore, to start using a plugin, you solely need to install it in the same environment tox is running in and it will be discovered via the defined entry-point (in the example above, tox will load your_plugin.hooks).

A plugin is created by implementing extension points in the form of hooks. For example the following code snippet would define a new --magic command line interface flag the user can specify:

from tox.config.cli.parser import ToxParser
from tox.plugin import impl


@impl
def tox_add_option(parser: ToxParser) -> None:
    parser.add_argument("--magic", action="store_true", help="magical flag")

You can define such hooks either in a package installed alongside tox or within a toxfile.py found alongside your tox configuration file (root of your project).

tox.plugin.NAME = 'tox'

the name of the tox hook

tox.plugin.impl

decorator to mark tox plugin hooks

tox.plugin.spec.tox_add_core_config(core_conf, state)

Called when the core configuration is built for a tox environment.

Parameters:
  • core_conf (ConfigSet) – the core configuration object

  • state (State) – the global tox state object

Return type:

None

tox.plugin.spec.tox_add_env_config(env_conf, state)

Called when configuration is built for a tox environment.

Parameters:
  • env_conf (EnvConfigSet) – the core configuration object

  • state (State) – the global tox state object

Return type:

None

tox.plugin.spec.tox_add_option(parser)

Add a command line argument.

This is the first hook to be called, right after the logging setup and config source discovery.

Parameters:

parser (ToxParser) – the command line parser

Return type:

None

tox.plugin.spec.tox_after_run_commands(tox_env, exit_code, outcomes)

Called after the commands set is executed.

Parameters:
  • tox_env (ToxEnv) – the tox environment being executed

  • exit_code (int) – exit code of the command

  • outcomes (list[Outcome]) – outcome of each command execution

Return type:

None

tox.plugin.spec.tox_before_run_commands(tox_env)

Called before the commands set is executed.

Parameters:

tox_env (ToxEnv) – the tox environment being executed

Return type:

None

tox.plugin.spec.tox_env_teardown(tox_env)

Called after a tox environment has been teared down.

Parameters:

tox_env (ToxEnv) – the tox environment

Return type:

None

tox.plugin.spec.tox_extend_envs()

Declare additional environment names.

Added in version 4.29.0.

This hook is called without any arguments early in the lifecycle. It is expected to return an iterable of strings with environment names for tox to consider. It can be used to facilitate dynamic creation of additional environments from within tox plugins.

This is ideal to pair with tox_add_core_config that has access to state.conf.memory_seed_loaders allowing to extend it with instances of tox.config.loader.memory.MemoryLoader early enough before tox starts caching configuration values sourced elsewhere.

Return type:

Iterable[str]

tox.plugin.spec.tox_on_install(tox_env, arguments, section, of_type)

Called before executing an installation command.

Parameters:
  • tox_env (ToxEnv) – the tox environment where the command runs in

  • arguments (Any) – installation arguments

  • section (str) – section of the installation

  • of_type (str) – type of the installation

Return type:

None

tox.plugin.spec.tox_register_tox_env(register)

Register new tox environment type. You can register:

  • run environment: by default this is a local subprocess backed virtualenv Python

  • packaging environment: by default this is a PEP-517 compliant local subprocess backed virtualenv Python

Parameters:

register (ToxEnvRegister) – a object that can be used to register new tox environment types

Return type:

None

Adopting a plugin under the tox-dev organization

You’re free to host your plugin on your favorite platform. However, the core tox development happens on GitHub under the tox-dev organization. We are happy to adopt tox plugins under the tox-dev organization if:

  • the plugin solves a valid use case and is not malicious,

  • it’s released on PyPI with at least 100 downloads per month (to ensure it’s actively used).

What’s in it for you:

  • you get owner rights on the repository under the tox-dev organization,

  • exposure of your plugin under the core umbrella,

  • backup maintainers from other tox plugin developers.

How to apply: