Free tools Windows power users keep installed
One-click scans. No signup required.
Some links on this page are affiliate links: if you buy through them we may earn a commission, at no extra cost to you.
Use pytest’s pytest_runtest_makereport hook in conftest.py (or a plugin). The hook receives a report after each test phase; run your method when the report is failed, and restrict it to report.when == "call" when you mean failures in the test body.
This runs after an assertion exception has stopped the test call—not between individual assert statements. If a test contains several assertions, the first uncaught failure ends that call, so later assertions are not reached.
The reliable pattern
Create a project-level conftest.py and implement a hook wrapper:
import pytest
@pytest.hookimpl(wrapper=True, tryfirst=True)
def pytest_runtest_makereport(item, call):
report = yield
if report.when == "call" and report.failed:
run_your_method(item, report)
Replace run_your_method with your diagnostic, cleanup, notification, or artifact code. The yield lets pytest execute the normal hook chain first; the resumed code can then inspect the completed report.
#1 Best Overall
What each condition means
report.when == "call"selects the test-body phase.report.failedis true when that phase failed.itemidentifies the collected test, including its node ID.callcontains call information and the exception details available during hook processing.
Keep the method defensive. A failure handler that raises its own exception can obscure the original test failure or create confusing secondary errors.
def run_your_method(item, report):
try:
print(f"FAILED: {item.nodeid}")
if report.longrepr:
print(report.longrepr)
# Save a screenshot, dump logs, or notify another system here.
except Exception as exc:
# Log the handler error without replacing the test's failure.
print(f"Failure hook error: {exc}")
Why this is not a callback after every assert
Python raises an exception when an assertion fails. Unless the test catches that exception, control leaves the test function immediately. pytest then records the failed call and creates a test report. pytest_runtest_makereport observes that report after the phase finishes; it cannot resume the test or execute code between two assertions.
To run code between assertions, the test itself must catch or structure the checks explicitly:
def test_values():
try:
assert first_condition()
finally:
method_after_first_check()
assert second_condition()
That approach changes test control flow and is different from a global failure hook. Use the report hook for actions that should happen once per failed test phase.
Choose the phase you want to monitor
pytest creates reports for setup, call, and teardown. Filtering only the call phase is usually correct for “the assertion failed in my test.” Remove or change that filter when setup and teardown failures must trigger the method too.
Rank #2
| Goal | Condition | Typical use |
|---|---|---|
| Failures in the test body only | report.when == "call" and report.failed |
Browser screenshot, request capture, test-specific diagnostics |
| Any failed phase | report.failed |
Central alerting or collecting all failure artifacts |
| Only setup failures | report.when == "setup" and report.failed |
Fixture and environment diagnostics |
| Only teardown failures | report.when == "teardown" and report.failed |
Cleanup and resource-leak investigation |
Because setup can fail before the test body starts and teardown can fail after it ends, phase selection is a functional decision, not cosmetic filtering.
Current and older wrapper syntax
Current pytest documentation uses the pluggy wrapper form @pytest.hookimpl(wrapper=True) with report = yield. Older examples use hookwrapper=True. The compatible spelling depends on the pytest and pluggy versions installed in your project, so check the versions before copying a snippet into a long-lived plugin.
Recommended Free Tools
# Older-style examples may look like this:
@pytest.hookimpl(hookwrapper=True, tryfirst=True)
def pytest_runtest_makereport(item, call):
outcome = yield
report = outcome.get_result()
if report.when == "call" and report.failed:
run_your_method(item, report)
Do not combine the two protocols in one implementation. If the wrapper form is rejected or the yielded value is not what the code expects, inspect the installed pytest/pluggy versions and follow the matching hook specification.
Where to put the hook
Project-local conftest.py
Put the file in the test directory or a parent directory that contains the tests. pytest discovers hooks in applicable conftest.py files; a file in one subtree is not automatically applied to unrelated tests elsewhere in the repository.
project/
├── conftest.py
├── src/
└── tests/
├── test_api.py
└── test_ui.py
A root-level file is convenient when every test should use the behavior. A nested file limits the hook to that directory subtree.
Reusable pytest plugin
Package the hook as a plugin when several repositories need the same behavior or when its supporting code deserves independent versioning and tests. A plugin also gives you a clear distribution boundary; conftest.py is simpler when the method is specific to one project.
| Choice | Scope | Best when | Trade-off |
|---|---|---|---|
conftest.py |
One test tree | The handler depends on local fixtures, paths, or services | Not automatically shared with other projects |
| Plugin | Multiple projects | You need distribution, versioning, and a stable interface | More packaging and compatibility maintenance |
A production-oriented implementation
The following version writes a compact failure record and preserves the original result. It demonstrates a node ID, phase check, and guarded handler:
from pathlib import Path
import pytest
ARTIFACTS = Path("test-artifacts")
def save_failure(item, report):
ARTIFACTS.mkdir(exist_ok=True)
safe_name = item.nodeid.replace("/", "_").replace("::", "__")
path = ARTIFACTS / f"{safe_name}.txt"
details = str(report.longrepr) if report.longrepr else "No long representation"
path.write_text(f"{item.nodeid}n{details}n", encoding="utf-8")
@pytest.hookimpl(wrapper=True, tryfirst=True)
def pytest_runtest_makereport(item, call):
report = yield
if report.when == "call" and report.failed:
try:
save_failure(item, report)
except Exception as exc:
# Avoid masking the assertion failure.
print(f"Could not save failure artifact: {exc}")
Use a unique naming scheme if parametrized tests or parallel workers can produce the same filename. For richer integrations, pass structured data to your logging or reporting system rather than scraping terminal output.
Related hooks that are easy to confuse
pytest_assertion_pass
pytest_assertion_pass is for successful assertions. It is opt-in through enable_assertion_pass_hook = true and is not a failure callback. Selecting it will not notify you when an assertion fails.
pytest_assertrepr_compare
pytest_assertrepr_compare customizes the explanation shown for comparison assertion failures. It formats diagnostics; it is not a general post-failure method hook.
Windows Errors? Fix Them Before They Spread
Repair common Windows errors and clear accumulated junk for a smoother, more stable PC - no reinstall needed.Free scan · no reinstallCrashes, No Sound, or Screen Glitches?
Random freezes, missing sound and display glitches usually trace back to one bad driver. Find and replace yours safely.Free scan · under a minuteRank #4
Troubleshooting
The hook never runs
- Confirm the
conftest.pyis in the test directory or one of its parents. - Run pytest from the intended project environment and verify the file is being loaded.
- Check that the function name is exactly
pytest_runtest_makereport. - Make sure the decorator syntax matches your installed pytest/pluggy versions.
The method runs for unexpected failures
Inspect report.when. Without a phase filter, setup and teardown failures also qualify. Add the call condition if only test-body assertions should trigger the method.
The original failure is hidden
Catch exceptions inside the handler and log them. Do not let screenshot, network, filesystem, or notification errors replace the assertion report.
It runs more than once
A test can produce separate reports for setup, call, and teardown. Filter to call, and ensure the hook is not implemented in both a plugin and an applicable conftest.py unless duplicate execution is intentional.
Later assertions are missing
That is normal for uncaught assertion failures. Split checks into separate tests, use parametrization, or catch an assertion deliberately if continuing is essential.
Parallel execution creates collisions
Include the worker identity or a unique test-run directory in artifact paths when using parallel workers. Also avoid mutable global state in the failure method.
Best Value
Performance, reliability, and cost considerations
Failure handlers run on the test-result path, so slow network calls can lengthen every failed test and make CI timeouts more likely. Prefer asynchronous job submission, bounded timeouts, and local buffering. Decide whether a failed handler should be retried, ignored, or fail the build; the safest default for diagnostics is to preserve the original test outcome while reporting handler errors separately.
Capture only the data needed for debugging. Screenshots, logs, and response bodies can contain credentials or personal information; redact secrets and restrict artifact access. For flaky tests, include the node ID, parameter values, phase, and timestamp so artifacts can be correlated without relying on terminal ordering.
Or skip the browser setup
If your failure method needs a webpage screenshot, ScreenshotNeo can perform the capture with one HTTP request. It accepts consent banners like a visitor and removes more than 60 known consent platforms, newsletter popups, and chat widgets before the shot. Bot checks, CAPTCHAs, blank pages, timeouts, failed loads, and cache hits are not billed, and the response identifies the result with X-Page-Verdict and X-Billed headers. Its MCP server provides take_screenshot, get_page_info, and capture_pdf tools for Claude, Cursor, and other MCP clients.
See the ScreenshotNeo documentation for the complete option list and response details.
cURL
curl -G "https://api.screenshotneo.com/v1/shot" -d access_key=YOUR_API_KEY --data-urlencode url=https://screenshotneo.com -o shot.webp
Python
import requests
r = requests.get("https://api.screenshotneo.com/v1/shot", params={"access_key": "YOUR_API_KEY", "url": "https://screenshotneo.com"}, timeout=90)
open("shot.webp", "wb").write(r.content)
Node.js
const q = new URLSearchParams({ access_key: 'YOUR_API_KEY', url: 'https://screenshotneo.com' });
const res = await fetch(`https://api.screenshotneo.com/v1/shot?${q}`);
ScreenshotNeo includes full-page and selector captures, device and retina settings, custom CSS and JavaScript, waits, request blocking, cookies and headers, caching, signed links, PDFs, bulk capture, asynchronous webhooks, and a usage API. The Free plan includes 1,000 shots per month without a card; paid plans start at $5 for 3,000 shots. Create a free ScreenshotNeo account to try it.
Frequently Asked Questions
Can I continue a test after an assertion fails?
Not with an uncaught assertion. Catch and handle the exception inside the test, or redesign the checks as separate tests or parametrized cases.
Should I use a hook or a fixture for failure screenshots?
Use the report hook when the action must run centrally after a failed phase. A fixture is useful when setup and teardown behavior is tied to a specific test resource.
Quick wins for a faster PC:
Clear out junk files and repair common Windows errorsFree Scan →Fix the driver behind crashes, sound loss and screen glitchesFind Drivers →Repair Windows errors before they cause bigger problemsFix Now →Why does a teardown failure trigger my handler?
pytest reports setup, call, and teardown separately. If your condition checks only report.failed, teardown failures are included; add a report.when filter to narrow it.
Quick Recap
Product prices and availability are accurate as of the date/time indicated and are subject to change. Any price and availability information displayed on Amazon at the time of purchase will apply.

