Writing hooks and local plugins

pytest · Expert · 15 min

Teams with large suites need custom behavior (opt-in slow tests, failure artifacts, ordering) that only hooks provide.

pytest is built from plugins communicating through hooks, implemented with the pluggy library. Any conftest.py is a local plugin: define a function with a hook's name (all start with pytest_) and pytest calls it at the right moment.

Frequently used hooks:

  • pytest_addoption(parser): add command-line flags and ini options, read later via config.getoption().
  • pytest_configure(config): one-time setup, e.g. registering markers with config.addinivalue_line("markers", ...).
  • pytest_collection_modifyitems(config, items): reorder, deselect or mark collected tests. The classic recipe skips slow tests unless --runslow is given.
  • pytest_runtest_makereport(item, call): build or inspect the report for each phase (setup, call, teardown). As a hook wrapper (@pytest.hookimpl(wrapper=True) and report = yield), you can attach the outcome to the item and save screenshots or logs on failure.
  • pytest_sessionfinish(session, exitstatus): summarize after the run.

Hook functions only receive the arguments they name, so you can declare a subset. Results of firstresult hooks stop at the first non-None return.

When local hooks are useful across projects, package them as a plugin module with a pytest11 entry point. pytester, a built-in fixture (enable with pytest_plugins = ["pytester"]), runs pytest in a temp directory to test your plugin.

Example

# conftest.py
import pytest

def pytest_addoption(parser):
    parser.addoption("--runslow", action="store_true", help="run slow tests")

def pytest_configure(config):
    config.addinivalue_line("markers", "slow: mark test as slow")

def pytest_collection_modifyitems(config, items):
    if config.getoption("--runslow"):
        return
    skip_slow = pytest.mark.skip(reason="need --runslow")
    for item in items:
        if "slow" in item.keywords:
            item.add_marker(skip_slow)

@pytest.hookimpl(wrapper=True)
def pytest_runtest_makereport(item, call):
    report = yield
    if report.when == "call" and report.failed:
        print(f"\nFAILED {item.nodeid}: save artifacts here")
    return report

Remember

  • conftest.py is a local plugin; define pytest_* hook functions
  • pytest_addoption + config.getoption for custom flags
  • pytest_collection_modifyitems to skip, mark or reorder tests
  • Hook wrappers (wrapper=True, yield) see the result of other hooks

Watch out for

  • Defining pytest_addoption in a nested conftest: options must be added in the rootdir conftest or a plugin loaded at startup.

Check yourself

  1. Which hook implements 'skip tests marked slow unless --runslow is given'?

Sign in to answer, see the explanations, and get this lesson back as a quick review later.

Official docs: docs.pytest.org/en/stable/how-to/writing_hook_functions.html