Errors & Reporting¶
Structured errors¶
A failing assertion raises AssertionFailure, a subclass of AssertionError that carries structured
data. Existing except AssertionError handlers keep working unchanged.
from assertpy2 import assert_that
try:
assert_that(1).is_equal_to(2)
except AssertionError as e:
print(e.actual) # 1
print(e.expected) # 2
For comparisons, a DiffResult with path-level entries is attached:
try:
assert_that({"a": 1, "b": 2}).is_equal_to({"a": 1, "b": 99})
except AssertionError as e:
print(e.diff)
# DiffResult(kind='dict', entries=[DiffEntry(path='b', actual=2, expected=99)])
Matcher-based assertions (matches_structure(), satisfies(), each()) attach a DiffResult with
kind='match', where each entry's expected holds the failed predicate's description.
When the pytest plugin is active (auto-registered via the pytest11 entry point, no configuration
needed), this data is rendered as extra report sections. See Rich pytest diffs
for supported types and configuration.
Rich pytest diffs¶
When is_equal_to() or contains()/contains_exactly() fail, the DiffResult on the exception is
rendered by the plugin as colored diff sections.
| Type | Diff kind | How it works |
|---|---|---|
list, tuple |
sequence |
Element-by-element, recursive into nested dicts/dataclasses/models |
set, frozenset |
set |
Extra and missing items |
str |
string |
Line-by-line comparison |
dict |
dict |
Key-by-key, recursive into nested dicts and lists |
dataclass |
dataclass |
Field-by-field, handles differing types with overlapping fields |
namedtuple |
namedtuple |
Field-by-field comparison |
| Pydantic model | model |
Field-by-field via model_dump(), recursive into nested models |
| other | scalar |
Single actual-vs-expected entry |
contains family |
contains |
Missing and extra items |
| matcher mismatch | match |
matches_structure() / satisfies() / each(): path + failed predicate |
--- AssertionFailure ---
actual: [{'id': 1, 'name': 'Alice'}, {'id': 2, 'name': 'Bob'}]
expected: [{'id': 1, 'name': 'Alice'}, {'id': 2, 'name': 'Robert'}]
The diff for that failure - and the other diff shapes - renders like this.
What each diff kind looks like¶
Value diffs (sequence, dict, dataclass, namedtuple, Pydantic model, string, scalar)
show the path with the removal in red and the addition in green - this is the diff for the example above:
Set and contains show extra items in red and missing items in green:
Match (matches_structure(), satisfies(), each()) shows each field's path and the predicate that
failed, with the actual value in red - every mismatch, not just the first (no green: a predicate has no
"addition"):
Nested structures are diffed recursively and report the exact path to the differing value (for example
[1].name). Circular references are detected and shown as <circular ref> rather than recursing forever.
The rich diff comes from the fluent form. The == drop-in for matchers (for example
assert response == {"id": match.is_positive()}) hands rendering to pytest instead, which prints its
own dict comparison without the path.
Configuration¶
[tool.pytest.ini_options]
assertpy2_diff = "off" # disable structured diff sections entirely
assertpy2_diff_max_entries = "100" # max entries to show (default 50, 0 = unlimited)
With --color=yes, diffs are colored: red removals, green additions, cyan headers. Entries beyond
the limit are hidden behind a ... and N more entries summary.
Failure and expected exceptions¶
fail()¶
Force a test failure explicitly:
Expected exceptions¶
For a called function, assert it raises and chain assertions on the message:
assert_that(some_func).raises(RuntimeError).when_called_with("foo")
assert_that(some_func).raises(RuntimeError).when_called_with("foo").is_equal_to("some err")
Or assert it does not raise a given exception:
Tip
For the common "did it raise?" case without inspecting the message, prefer pytest's
pytest.raises context manager.
Expected warnings¶
For a called function, assert it emits a warning and chain assertions on the warning message:
assert_that(deprecated_func).warns(DeprecationWarning).when_called_with("foo")
assert_that(deprecated_func).warns(DeprecationWarning).when_called_with("foo").matches("since 2.6")
The category defaults to Warning (matches any warning) and matches subclasses. Or assert it does
not emit a given category:
To also assert on the value the call returned (alongside the warning, or after does_not_warn /
does_not_raise), pivot with returned():
assert_that(make_client).warns(DeprecationWarning).when_called_with().returned().is_instance_of(Client)
assert_that(adder).does_not_raise(TypeError).when_called_with(1, 2).returned().is_equal_to(3)
returned() exposes the type-agnostic core assertions (is_equal_to, is_instance_of, satisfies,
...); it raises TypeError if the call raised (there is no return value to inspect).
Not thread-safe
warns() / does_not_warn() rely on warnings.catch_warnings(), which mutates process-global
state. They are safe within a single thread (including multiple asyncio tasks on one event
loop), but concurrent use across OS threads can interfere - the same limitation as
pytest.warns and unittest.assertWarns.
Custom error messages¶
described_as() prepends a custom label to the failure message:
assert_that(1 + 2).described_as("adding stuff").is_equal_to(2)
# [adding stuff] Expected <3> to be equal to <2>, but was not.
Warnings instead of failures¶
For defensive assertions outside tests, replace assert_that with assert_warn: failures log a
warning instead of raising:
assert_warn() vs warns()
These are unrelated despite the similar names. assert_warn(...) is a soft entry point: the
assertion still checks your value, but logs a warning instead of raising on failure.
assert_that(func).warns(...) is the opposite direction - it asserts that calling func
emits a Python warning.