Skip to main content

        Hooks and plugins turn pytest from a runner into a customizable platform. Here's how they work and how to package one.

Extending pytest With Hooks and Plugins

Hooks and plugins turn pytest from a runner into a customizable platform. Here's how they work and how to package one.

It’s no secret that I believe pytest is one of the most powerful Python-based test frameworks. One of its great features is the ability to be extended through hooks and plugins. Hooks let you tap into specific points during the testing process, while plugins provide a way to bundle hooks and other pytest extensions into a single distributable package.

Utilizing hooks

Hooks are a way to extend or modify pytest’s behavior without modifying the core pytest code. pytest provides numerous hooks that you can define and implement in your tests. Here’s an example of using the pytest_runtest_setup hook to perform an action before each test runs:

import pytest

def pytest_runtest_setup(item):
    # Called before each test is run
    print(f"Setting up test: {item.name}")

In this example, the pytest_runtest_setup hook prints a message before each test is executed. pytest automatically discovers and calls this hook function during the test run.

Creating a plugin

While hooks are useful for extending pytest in a single project, plugins let you package your hooks and other extensions into a distributable package that can be shared across multiple projects.

To create a plugin, you’ll need to create a Python package and define the entry points for your plugin. Here’s a minimal example:

# hooks.py
import pytest

def pytest_runtest_setup(item):
    print(f"Setting up test: {item.name}")

In setup.py, define the entry point:

# setup.py
from setuptools import setup

setup(
    name="my_pytest_plugin",
    packages=["my_pytest_plugin"],
    entry_points={
        "pytest11": ["my_plugin = my_pytest_plugin.hooks"]
    }
)

Once your plugin is installed, pytest will automatically discover and load it.

Example: a performance monitoring plugin

Let’s create a plugin that measures the execution time of each test and prints a report at the end of the run. First, create a new directory:

pytest_perf_monitor/
    __init__.py
    plugin.py
setup.py

In plugin.py, define your hooks and helpers:

# plugin.py
import time
from typing import Dict

test_durations: Dict[str, float] = {}

def pytest_runtest_setup(item):
    test_durations[item.nodeid] = time.time()

def pytest_runtest_teardown(item):
    start_time = test_durations.pop(item.nodeid)
    duration = time.time() - start_time
    print(f"Test '{item.nodeid}' took {duration:.6f} seconds")

def pytest_terminal_summary(terminalreporter):
    print("\nTest Durations:")
    for test_id, duration in test_durations.items():
        print(f"  {test_id}: {duration:.6f} seconds")

In setup.py, define the entry point:

# setup.py
from setuptools import setup

setup(
    name="pytest_perf_monitor",
    packages=["pytest_perf_monitor"],
    entry_points={
        "pytest11": ["perf_monitor = pytest_perf_monitor.plugin"]
    }
)

After installing your plugin, pytest automatically loads it, and you’ll see the execution time for each test plus a summary report at the end.

Creating a pytest plugin with Cookiecutter

You can also use cookiecutter-pytest-plugin, a Cookiecutter template that scaffolds a pytest plugin with a consistent structure and best practices out of the box.

I’ll add that personally, I find creating plugins manually is faster once you know the basics, but it doesn’t hurt to learn another way.

Conclusion

Hooks and plugins are powerful features that let you extend and customize pytest’s behavior. Hooks tap into specific points during the testing process, while plugins bundle them into distributable packages. By leveraging these features, you can enhance your testing workflow and gain greater control over the testing process.