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
Firstresult hooks
Hook wrappers
Hook function ordering
Declaring new hooks
Using hooks in
pytest_addoptionOptionally using hooks from third-party plugins
Storing data on pytest Items across hooks
Hook Function Validation and Execution
pytest calls hook functions from all registered plugins based on defined hook specifications.
Key Points:
Hook function parameters must match the hook specification argument names exactly.
pytest dynamically prunes arguments passed to a hook function, sending only those declared in the function signature.
This enables backward and forward compatibility, allowing pytest to add new hook parameters without breaking existing plugins.
Hook functions other than those starting with
pytest_runtest_must not raise exceptions; doing so breaks the pytest run.
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:
Marked with
@pytest.hookimpl(wrapper=True).Must yield exactly once.
Receive the same arguments as the underlying hook.
At the
yieldpoint, pytest executes the next hook(s) and returns their result to the yield expression.The wrapper can modify or replace the result, handle exceptions, or perform side effects before and after the wrapped hooks.
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:
A minimal hook wrapper is
return (yield), which simply passes through the result.Wrappers can also catch exceptions around the
yieldto suppress or transform errors.For detailed behavior, see the pluggy hookwrappers documentation.
Hook Function Ordering / Call Example
Multiple plugins can implement the same hook, and pytest calls them in a specific order.
Ordering Options:
tryfirst=True: Execute the hook implementation as early as possible.trylast=True: Execute the hook implementation as late as possible.wrapper=True: Execute as a hook wrapper, wrapping all other non-wrapper hooks.
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:
Plugin 3's hook wrapper runs up to the
yield.Plugin 1's hook runs.
Plugin 2's hook runs.
Plugin 3 resumes after
yieldand 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:
Hook names must start with
pytest_.Hooks are declared as no-op functions with docstrings describing their API.
Hooks must be registered with pytest's
pluginmanagerby callingadd_hookspecs().
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:
Create a small plugin class implementing the third-party hooks.
Conditionally register this plugin during
pytest_configure()only if the other plugin is present.
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:
Type checkers like mypy dislike dynamic attribute assignment.
Possible name clashes with other plugins.
Recommended Solution: Using item.stash
stashis a dictionary-like storage attached to all pytest nodes (items, classes, sessions, config).Use typed
StashKeyobjects to store/retrieve data safely.
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:**
pytest calls a hook, which may have multiple implementations.
Hook wrappers run first, yielding control to non-wrapper hooks.
Non-wrapper hooks run in order:
tryfirst, normal, thentrylast.The result is returned to the wrapper which can process or modify it.
Finally, the overall hook call returns the result.
Summary
This file provides a detailed guide to writing and managing pytest hook functions, including:
How pytest validates and calls hooks.
The use of
firstresultfor early exit on successful hook results.Writing hook wrappers to wrap around other hooks.
Controlling execution order with
tryfirstandtrylast.Declaring new hooks and using them in plugins.
Handling optional third-party hooks gracefully.
Managing plugin state safely with
item.stash.
By following these conventions and patterns, plugin authors can create robust, compatible, and maintainable pytest extensions.
**End of writing_hook_functions.rst**