Skip to content
Back to top

# 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.

## Testing plugins

tox ships a pytest plugin at `tox.pytest` with fixtures for spinning up isolated tox projects, faking package indexes, and capturing output. To use it, declare a dependency on the `testing` extra so that `pytest`, `pytest-mock` and `devpi-process` are pulled in:

    [dependency-groups]
    test = [
      "tox[testing]",
    ]

Then register the plugin in your `conftest.py`:

    pytest_plugins = "tox.pytest"

## 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:

- create an issue under the `tox-dev/tox` GitHub repository with the title Adopt plugin <name>,

- wait for the green light by one of the maintainers (see Current maintainers),

- follow the guidance by GitHub,

- (optionally) add at least one other person as co-maintainer on PyPI.