pytest.fail with message vs assert vs xfail (tested)
Call pytest.fail("message") to fail a test with your own message (pytrace=False hides the traceback). For value comparisons prefer assert a == b, "message". For tests that are expected to fail, use @pytest.mark.xfail(reason=..., strict=True).
Three ways to make a test fail with your own message, and the two xfail tools for tests that are expected to fail. One file, run once on pytest 9.1.1:
$ cat test_f.py
import pytest
def test_fail_msg():
pytest.fail("config file missing: settings.toml")
def test_fail_no_trace():
pytest.fail("API returned 500", pytrace=False)
def test_assert_msg():
assert 1 == 2, "values differ"
@pytest.mark.xfail(reason="bug #12")
def test_known_bug():
assert 1 == 2
@pytest.mark.xfail(reason="bug #13")
def test_fixed_bug():
assert 1 == 1
@pytest.mark.xfail(reason="bug #14", strict=True)
def test_fixed_strict():
assert 1 == 1
def test_xfail_call():
pytest.xfail("not supported on this platform")
assert False
$ pytest -q -rA | sed -n "/short test summary/,$ p"; echo "exit code: ${PIPESTATUS[0]}"
=========================== short test summary info ============================
XFAIL test_f.py::test_known_bug - bug #12
XFAIL test_f.py::test_xfail_call - not supported on this platform
XPASS test_f.py::test_fixed_bug - bug #13
FAILED test_f.py::test_fail_msg - Failed: config file missing: settings.toml
FAILED test_f.py::test_fail_no_trace - Failed: API returned 500
FAILED test_f.py::test_assert_msg - AssertionError: values differ
FAILED test_f.py::test_fixed_strict - [XPASS(strict)] bug #14
4 failed, 2 xfailed, 1 xpassed in 0.02s
exit code: 1
1. pytest.fail("message")
$ pytest -q -k fail_msg | grep -E "^>|^E "
> pytest.fail("config file missing: settings.toml")
E Failed: config file missing: settings.toml
$ pytest -q -k no_trace | sed -n "3,5p"
______________________________ test_fail_no_trace ______________________________
API returned 500
=========================== short test summary info ============================
pytest.fail(msg) fails the test immediately and the summary shows Failed: <msg>. Use it for conditions that are not a simple comparison (a missing file, an unexpected branch). With pytrace=False pytest prints only the message, no traceback, which is cleaner for "this is the whole explanation" failures such as a bad API status.
2. pytest.fail vs assert ..., "message"
assert a == b, "values differ" shows AssertionError: values differ and keeps pytest's comparison details in the traceback; prefer it whenever you compare values. pytest.fail is for when there is nothing to compare. assert False, "msg" works too but reads worse than pytest.fail("msg").
3. pytest.mark.xfail: expected failures
A failing xfail test is reported as XFAIL with its reason and does not fail the run. If it unexpectedly passes it is XPASS, which also does not fail the run (test_fixed_bug), so a fixed bug can go unnoticed. With strict=True an unexpected pass becomes FAILED [XPASS(strict)] (test_fixed_strict); set xfail_strict = true in the config to make that the default.
4. pytest.xfail() inside the test
Calling pytest.xfail("reason") stops the test at that line and marks it XFAIL; code after it (assert False) never runs. Use it when you only know at run time that the test cannot pass (platform, missing service). For "do not run at all", use pytest.skip instead.
Here the run exits with code 1 because of the four FAILED tests; XFAIL and XPASS alone would exit 0 (see pytest exit codes). Related: pytest Failed: DID NOT RAISE.
Frequently asked questions
How do I fail a pytest test with a custom message?
Call pytest.fail('message'). The summary shows Failed: message; pass pytrace=False to print only the message.
What is the difference between pytest.fail and xfail?
pytest.fail makes the test FAIL. xfail marks it as expected to fail: reported as XFAIL and not counted as a failure.
Why does an xfail test that passes not fail the run?
A passing xfail test is XPASS, which is not a failure unless strict=True or xfail_strict = true is set.
Written with AI assistance. Measured on 2026-10-23 with pytest 9.1.1 on Python 3.13.5 in a clean virtualenv. Each block was run verbatim and the output pasted unedited. PlainHeaders is our extension; Python and pytest are free.