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.