When and why should I use `@pytest.hookimpl(hookwrapper=True)` with `pytest_runtest_setup` and other `pytest_runtest_*` hooks?

In conftest.py, I implemented pytest_runtest_setup, pytest_runtest_call, and pytest_runtest_teardown to execute code before and after test phases. For example:

def pytest_runtest_call(item: Any) -> None:
    item.session.test_info["on_case_number"] += 1
    pytest_log = getLogger("runtest")
    pytest_log.debug(f'= runtest started [{count_str(item.session.test_info)}]')
    outcome = yield
    setattr(item, "case_markers", item.own_markers)
    # process results and exceptions
    pytest_log.debug(f'= runtest completed [{count_str(item.session.test_info)}]')

However, this function never runs unless I add @pytest.hookimpl(hookwrapper=True) above it:

@pytest.hookimpl(hookwrapper=True)
def pytest_runtest_call(item: Any) -> None:
    ...

I thought certain pytest_runtest_* hooks would be called automatically without needing the hookwrapper=True decorator (e.g., pytest_collection_modifyitems works without it).

Why is this necessary, and when should hookwrapper=True be used for these hooks like pytest_runtest_setup?

The reason your code didn’t run without hookwrapper=True is that pytest_runtest_* hooks (like pytest_runtest_setup and pytest_runtest_call) are phase hooks that expect a generator if you want to wrap the execution of the test.

When you use hookwrapper=True, pytest allows your hook to yield control, run the original implementation, and then continue processing afterward:

@pytest.hookimpl(hookwrapper=True)
def pytest_runtest_call(item):
    # code before the test runs
    outcome = yield
    # code after the test runs

Hooks like pytest_collection_modifyitems don’t need a wrapper because they run once at collection time and don’t wrap any further execution—they don’t yield control to another phase.

Use hookwrapper=True when you want to inject behavior before and after a standard pytest phase. For example, with pytest_runtest_setup, pytest_runtest_call, and pytest_runtest_teardown, using a wrapper allows you to:

  • Run setup or logging before the actual test phase.

  • Capture outcomes, exceptions, or timing after the phase completes.

Without hookwrapper=True, your function would just run in place without access to the actual test execution, which is why your logging and custom processing wouldn’t work.

Think of hookwrapper=True as giving you a “sandwich” around the test phase. You get to run code before, let pytest do its usual work (setup, call, teardown), and then run code after. Example pattern:

import pytest

@pytest.hookimpl(hookwrapper=True)
def pytest_runtest_setup(item):
    print("Before setup")
    outcome = yield  # lets pytest continue with the actual setup
    print("After setup")

I like this approach because it keeps logging, metrics, or custom behavior neatly wrapped around the standard test lifecycle without interfering with pytest’s normal execution.