Writing and Reporting Assertions in pytest 8.x

Utilizing Standard assert Statements

pytest enhances the standard Python assert statement, allowing you to use built-in Python constructs without boilerplate code while maintaining detailed introspection. For instance, to verify a function's return value:

# content of test_calculation.py
def calculate_product(x, y):
    return x * y

def test_multiplication():
    assert calculate_product(3, 4) == 15

If this assertion fails, pytest provides a detailed report showing the intermediate values:

$ pytest test_calculation.py
=========================== test session starts ============================
platform linux -- Python 3.x.y, pytest-8.x.y, pluggy-1.x.y
rootdir: /home/user/project
collected 1 item

test_calculation.py F                                                 [100%]

================================= FAILURES =================================
_________________________ test_multiplication _____________________________

    def test_multiplication():
>       assert calculate_product(3, 4) == 15
E       assert 12 == 15
E        +  where 12 = calculate_product(3, 4)

test_calculation.py:6: AssertionError
========================= short test summary info ==========================
FAILED test_calculation.py::test_multiplication - assert 12 == 15
============================ 1 failed in 0.12s =============================

You can also append a custom message to the assertion:

assert a % 2 == 0, "value was odd, should be even"

Asserting Expected Exceptions

To validate that code throws a specific exception, use pytest.raises() as a context manager. This is more precise than using @pytest.mark.xfail.

import pytest

def validate_age(age):
    if age < 0:
        raise ValueError("Age cannot be negative")

def test_negative_age():
    with pytest.raises(ValueError):
        validate_age(-1)

To access the thrown exception instance, assign the context manager to a variable:

def test_exception_message():
    with pytest.raises(ValueError) as exc_info:
        validate_age(-5)
    
    # Check the string representation of the exception
    assert "negative" in str(exc_info.value)

Note that pytest.raises matches subclasses as well. To ensure a specific exact type is raised, you can add a secondary assertion:

def test_specific_exception_type():
    with pytest.raises(ValueError) as exc_info:
        validate_age(-1)
    
    # Ensure it is not a subclass (though ValueError rarely has relevant subclasses in this context)
    assert exc_info.type is ValueError

Matching Exception Messages

The match parameter allows you to use regular expressions to verify the exception message content, similar to unittest.TestCase.assertRaisesRegex.

def trigger_error():
    raise RuntimeError("System code 500: internal error")

def test_match_regex():
    with pytest.raises(RuntimeError, match=r"System code \d+"):
        trigger_error()

This uses re.search() under the hood, so the pattern does not need to match the entire string.

Working with Exception Groups

For code that raises ExceptionGroup (introduced in Python 3.11), pytest provides the group_contains() method to verify specific exceptions within the group.

def test_exception_group_content():
    with pytest.raises(ExceptionGroup) as exc_info:
        raise ExceptionGroup(
            "Multiple Failures",
            [
                ValueError("Invalid input"),
                TypeError("Wrong type")
            ]
        )
    
    # Check if ValueError is present
    assert exc_info.group_contains(ValueError)
    
    # Check if specific message is present for TypeError
    assert exc_info.group_contains(TypeError, match=r"Wrong type")

You can also control the search depth using the depth argument if you have nested groups.

Legacy pytest.raises Syntax

Before context managers were standard, pytest.raises accepted a callable and its arguments. This form is still supported but less readable.

# Legacy approach: passing the function and arguments directly
pytest.raises(ValueError, validate_age, age=-1)

Comparing xfail and raises

Use pytest.mark.xfail(raises=...) when documenting a known bug where the test fails due to an exception. Use pytest.raises() when actively testing that your code correctly throws exceptions for invalid inputs.

@pytest.mark.xfail(raises=RuntimeError)
def test_known_bug():
    # This test is expected to fail until the bug is fixed
    unstable_external_call()

Context-Aware Comparisons

pytest provides rich comparison details for standard data structures. For example, comparing two sets:

def test_set_equality():
    expected = {"apple", "banana", "cherry"}
    actual = {"apple", "banana", "date"}
    assert expected == actual

The failure output will explicitly list items missing or extra in either set. Similar intelligence aplies to long strings (showing diffs), lists (showing first index of difference), and dictionaries (showing differing keys).

Defining Custom Explanations

Through the pytest_assertrepr_compare hook, you can define custom failure messages for specific object types.

# conftest.py
class Transaction:
    def __init__(self, id, amount):
        self.id = id
        self.amount = amount
    
    def __eq__(self, other):
        return self.id == other.id and self.amount == other.amount

def pytest_assertrepr_compare(config, op, left, right):
    if isinstance(left, Transaction) and isinstance(right, Transaction) and op == "==":
        return [
            "Transaction comparison failed:",
            f"  IDs: {left.id} vs {right.id}",
            f"  Amounts: {left.amount} vs {right.amount}",
        ]

With the hook above, a failing test will display the custom breakdown instead of a generic object representation.

Assertion Introspection Mechanics

pytest achieves detailed reporting by rewriting assert statements during module import. This process inserts introspection logic into the bytecode. By default, pytest rewrites modules in its test collection path. To enable rewriting for non-test modules, use pytest.register_assert_rewrite before importing them.

Disabling Assertion Rewriting

If the import hook interferes with your system, you can disable rewriting globally via the command line:

pytest --assert=plain

Alternatively, to disable it for a specific module, add PYTEST_DONT_REWRITE to the module's docstring.

Caching Behavior

pytest caches rewritten modules to disk as .pyc files. To prevent writing these cache files (e.g., to keep a clean directory), set sys.dont_write_bytecode = True in your conftest.py:

import sys
sys.dont_write_bytecode = True

This disables disk caching but retains the introspection capabilities during runtime.

Tags: pytest python testing assertions exceptions

Posted on Sun, 14 Jun 2026 17:03:33 +0000 by talkster5