writing_hook_functions.rst

Overview

This documentation file explains the concept, usage, and implementation details of **hook functions** in the pytest testing framework. Hook functions are user-defined callbacks invoked by pytest at specific points during test collection, execution, or other lifecycle events. This file serves as a comprehensive guide on how pytest manages hooks, how to write and validate hook functions, how to control their execution order, and how to declare new hooks for plugin extension.

The document also covers advanced topics such as hook wrappers (generator-based hooks that wrap other hooks), firstresult hooks (which stop execution after the first non-None result), inter-plugin hook interactions, and best practices for storing data on pytest test items across hooks.

This file is primarily aimed at plugin developers and advanced pytest users who want to customize or extend pytest behavior through hooks.

Contents


Hook Function Validation and Execution

pytest calls hook functions from all registered plugins based on defined hook specifications.

Key Points:

Example:

def pytest_collection_modifyitems(config, items):
    # This hook runs after test collection is complete.
    # You receive `config` and `items` as parameters.
    # Note: The `session` argument is omitted here and thus not passed.
    ...

Firstresult: Stop at First Non-None Result

Some hooks are declared with `firstresult=True`. When such a hook is called, pytest executes registered hook functions sequentially until one returns a **non-None** result. After that, remaining hooks are skipped, and the first non-None result is used as the overall hook result.

This optimizes performance and enforces a single authoritative result when multiple plugins implement the same hook.


Hook Wrappers: Executing Around Other Hooks

Hook wrappers are special hook implementations designed as generator functions that wrap the execution of other hooks.

Characteristics:

Example:

import pytest

@pytest.hookimpl(wrapper=True)
def pytest_pyfunc_call(pyfuncitem):
    do_something_before_next_hook_executes()

    # Execute the wrapped hooks and get their result or exception.
    res = yield

    new_res = post_process_result(res)

    # Return the modified result to pytest.
    return new_res

Notes:


Hook Function Ordering / Call Example

Multiple plugins can implement the same hook, and pytest calls them in a specific order.

Ordering Options:

Example:

# Plugin 1 (runs early)
@pytest.hookimpl(tryfirst=True)
def pytest_collection_modifyitems(items):
    ...

# Plugin 2 (runs late)
@pytest.hookimpl(trylast=True)
def pytest_collection_modifyitems(items):
    ...

# Plugin 3 (hook wrapper, runs around others)
@pytest.hookimpl(wrapper=True)
def pytest_collection_modifyitems(items):
    try:
        return (yield)  # Execute non-wrapper hooks
    finally:
        # Runs after all non-wrapper hooks
        ...

Execution Order:

  1. Plugin 3's hook wrapper runs up to the yield.

  2. Plugin 1's hook runs.

  3. Plugin 2's hook runs.

  4. Plugin 3 resumes after yield and runs its final code.

`tryfirst` and `trylast` can also be used on hook wrappers to control their relative order.


Declaring New Hooks

Plugins and `conftest.py` files can declare new hook specifications that other plugins may implement.

Guidelines:

Example Hook Declaration (sample_hook.py):

def pytest_my_hook(config):
    """
    Receives the pytest config and performs custom actions.
    """

Registering Hooks:

def pytest_addhooks(pluginmanager):
    import sample_hook
    pluginmanager.add_hookspecs(sample_hook)

Calling Hooks:

Hooks can be called from fixtures or other hooks via the `hook` object on `config` or `pytestconfig`.

@pytest.fixture()
def my_fixture(pytestconfig):
    result = pytestconfig.hook.pytest_my_hook(config=pytestconfig)

Implementing Hooks:

Other plugins implement the new hook by defining a function with the same name and signature.

def pytest_my_hook(config):
    print(config.hook)

Using Hooks in pytest_addoption

Hooks can be used to customize command-line options dynamically.

Scenario:

A plugin wants to define a command-line option with a default value provided by another plugin.

Example:

# hooks.py

from pluggy import hookspec

@hookspec(firstresult=True)
def pytest_config_file_default_value():
    """Return the default value for a config file option."""

# myplugin.py

def pytest_addhooks(pluginmanager):
    from . import hooks
    pluginmanager.add_hookspecs(hooks)

def pytest_addoption(parser, pluginmanager):
    default_value = pluginmanager.hook.pytest_config_file_default_value()
    parser.addoption(
        "--config-file",
        help="Config file to use, defaults to %(default)s",
        default=default_value,
    )

# conftest.py

def pytest_config_file_default_value():
    return "config.yaml"

Optionally Using Hooks from 3rd Party Plugins

Sometimes you want to implement hooks only if a certain third-party plugin is installed.

Best Practice:

Example:

class DeferPlugin:
    def pytest_testnodedown(self, node, error):
        # xdist hook implementation

def pytest_configure(config):
    if config.pluginmanager.hasplugin("xdist"):
        config.pluginmanager.register(DeferPlugin())

This avoids validation failures if the dependent plugin is missing and allows conditional hook installation.


Storing Data on Items Across Hook Functions

Plugins often need to share state or data on test items across multiple hooks.

Problems with Direct Attribute Assignment:

Recommended Solution: Using item.stash

Example:

# Define stash keys at module level
been_there_key = pytest.StashKey[bool]()
done_that_key = pytest.StashKey[str]()

def pytest_runtest_setup(item: pytest.Item) -> None:
    item.stash[been_there_key] = True
    item.stash[done_that_key] = "no"

def pytest_runtest_teardown(item: pytest.Item) -> None:
    if not item.stash[been_there_key]:
        print("Oh?")
    item.stash[done_that_key] = "yes!"

The stash provides a safe and conflict-free way to store plugin-specific data on pytest nodes.


Visual Diagram: Hook Function Structure and Flow

flowchart TD
    A[pytest hook call] --> B{Multiple hook implementations?}
    B -- Yes --> C[Execute hook wrappers in order]
    C --> D[Yield to execute non-wrapper hooks]
    D --> E{Hooks with tryfirst/trylast?}
    E -- Execute tryfirst --> F[Run tryfirst hooks]
    F --> G[Run normal hooks]
    G --> H[Run trylast hooks]
    H --> I[Return results to wrapper after yield]
    I --> J[Wrapper post-processing]
    J --> K[Final hook result]
    B -- No --> L[Execute single hook implementation]
    L --> K

**Diagram Explanation:**


Summary

This file provides a detailed guide to writing and managing pytest hook functions, including:

By following these conventions and patterns, plugin authors can create robust, compatible, and maintainable pytest extensions.


**End of writing_hook_functions.rst**