Skip to content

Structured failures

The exception raised when an assertion fails, carrying the structured diff.

AssertionFailure subclasses AssertionError, so existing except AssertionError handlers keep working unchanged. See Errors & reporting for usage.

AssertionFailure

AssertionFailure(
    message: str,
    *,
    actual: object = None,
    expected: object = None,
    diff: DiffResult | None = None,
    trace: PollTrace | None = None,
    requirement: Requirement | None = None,
    failures: tuple[AssertionOutcome, ...] = (),
)

Structured assertion failure with optional diff data.

Subclasses AssertionError for full backward compatibility: existing except AssertionError handlers catch it transparently.

Source code in assertpy/errors.py
def __init__(
    self,
    message: str,
    *,
    actual: object = None,
    expected: object = None,
    diff: DiffResult | None = None,
    trace: PollTrace | None = None,
    requirement: Requirement | None = None,
    failures: tuple[AssertionOutcome, ...] = (),
):
    super().__init__(message)
    self._message = message
    self.actual = actual
    self.expected = expected
    self.diff = diff
    self.trace = trace
    self.requirement = requirement
    """What was asked of the value, as data: the operation, its parameters, and whether it was negated.

    ``None`` where no operation was asked: `fail()`, a bare `error()` carrying a message of the
    caller's own, and a precondition of one of the few members that assert nothing on their own.
    """
    self.failures = failures
    """The failures a soft block collected, in the order they were collected.

    Empty on a failure that is about one value, which is every failure except the aggregate a
    ``soft_assertions()`` block raises when it closes.  The aggregate's message is these rendered
    into a list; this is the same thing before it became a string.
    """
    self._compared: tuple[object, object] | None = None
    """The two values a failed equality held against each other, or ``None`` for any other failure.

    The value and the operand, or under ``ignore=`` / ``include=`` the copies without the keys left out:
    the pair ``==`` alone decided.  Read by the pytest plugin for a listener that shows two values side
    by side.
    """
    self._compared_pairs: tuple[tuple[object, object] | None, ...] = ()
    """Beside each of `failures`, the pair its equality held against each other, or ``None``.

    Under ``ignore=`` or ``include=`` that is the two copies without the keys left out, which a raised failure
    keeps as ``_compared``: the pytest plugin shows it, so a collected failure reads as the raised one does.
    Kept here rather than on the record so the published record's fields stay what they were.
    """
    self._soft_summary: Callable[[], str] | None = None
    """Draws a soft block's summary with each failure's diff in block form, for a message nobody else renders.

    Set by the block that raises this, and called at each read with the switch on, so the text shows the diffs as
    they are then, as a single failure's diff does.  Drawing it when raised cost a caught failure nobody printed
    8 us for one failure and 100 us for twenty.
    """
    self._outcome: AssertionOutcome | None = None
    """The record this failure was composed from, set by the delivery half of `error()`.

    Carries what the flat attributes cannot: whether ``actual`` and ``expected`` were named by the
    assertion or filled in from the value under test, which is what `has_expected` reads.  The
    polling and snapshot re-wraps carry the record of the failure they re-wrap.  It stays ``None``
    on a failure built without one, which `fail()` and the aggregate a soft block raises both are.

    Private, and not a constructor argument, because `AssertionOutcome` is still gaining a field
    per release.  It becomes public when a caller outside this package has a reason to read it.
    """

requirement instance-attribute

requirement = requirement

What was asked of the value, as data: the operation, its parameters, and whether it was negated.

None where no operation was asked: fail(), a bare error() carrying a message of the caller's own, and a precondition of one of the few members that assert nothing on their own.

failures instance-attribute

failures = failures

The failures a soft block collected, in the order they were collected.

Empty on a failure that is about one value, which is every failure except the aggregate a soft_assertions() block raises when it closes. The aggregate's message is these rendered into a list; this is the same thing before it became a string.

has_expected property

has_expected: bool

Whether the assertion named an expected value at all.

expected is None cannot answer it: is_equal_to(None) names one and is_not_empty() names none, and both leave the attribute at None. A reporter deciding whether to show an expected column needs the difference, and had to read the private record to get it.

Read from the record a failure composed through error() carries, which the polling and snapshot re-wraps preserve. A failure built directly has none, and fail() and the aggregate a soft block raises are built that way: there the older reading is all there is, and an expectation counts as named when it is not None. Both of those name none and hold None, so the fallback answers them correctly.

DiffResult dataclass

DiffResult(*, kind: str, entries: list[DiffEntry] = list())

Structured diff between two values.

kind names the diff category - the shape of comparison that produced the entries. It is one of "dict", "sequence", "dataclass", "namedtuple", "model", "attrs", "set", "string", "scalar", "contains", "match", or "openapi".

It is unrelated to the assertion builder's kind argument, which selects the failure mode (None/"soft"/"warn").

Step

One hop from a value to one of its parts, as DiffEntry.steps records it.

path is written for a person and is lossy by construction: a mapping key goes through str(), so {3: ...} and {"3": ...} land on the same text, and a key holding a dot or a bracket cannot be read back out. A step keeps the key itself, so a reader can walk back into the value it came from instead of parsing a string that was never a grammar.

kind instance-attribute

kind: Literal[
    "key", "index", "attr", "item", "line", "json"
]

What kind of hop this is.

key indexes a mapping, index a sequence, attr reads a field of a dataclass, namedtuple, attrs class or model. item names a member of a set or of another collection that keeps no positions, and the mapping key a difference lies inside. line is the 1-based line number of a text or bytes diff. json stands where the value is JSON text and the difference lies in what it holds: the steps after it lead into what the text decodes to.

value instance-attribute

value: object

The key, index, field name, member or line number. Not stringified: that is the whole point.

For json it is what the text decodes to, the very object the steps after it lead into: decoding the text again would give an equal value and another object.

side class-attribute instance-attribute

side: Literal['actual', 'expected'] | None = None

Which sequence the index belongs to, when the two have shifted apart.

Sequence alignment reports an inserted element against one side only, and once the two index spaces disagree an index without a side names two different elements. None whenever both sides share the position, which is every step that is not a one-sided element of an aligned sequence.

DiffEntry dataclass

DiffEntry(
    *,
    path: str,
    actual: object = None,
    expected: object = None,
    absent: Literal["actual", "expected"] | None = None,
    steps: tuple[Step, ...] = (),
)

Single difference between actual and expected values at a specific path.

absent class-attribute instance-attribute

absent: Literal['actual', 'expected'] | None = None

Which side had no value here at all, as opposed to holding None.

Without this the two are indistinguishable, since both leave the field at None, and a dictionary compared against one whose value is None renders exactly like a dictionary with an extra key. Readers of a diff have to be able to tell "this key is not there" from "this key is there and its value is None", and so does anything reasoning about the diff afterwards.

Defaults to None, so an entry built the old way keeps its old meaning and only the producers that mean absence say so.

steps class-attribute instance-attribute

steps: tuple[Step, ...] = ()

The same location as path, in the form a program can use.

Empty at the root, which is the entry path renders as .: the difference is the whole value. Also empty on an entry whose path is a label rather than a location, which is what a containment or matcher failure produces.

PollTrace dataclass

PollTrace(
    *,
    samples: list[PollSample],
    total_polls: int,
    dropped: int,
    elapsed: float,
    summary: str,
)

Convergence telemetry attached to an eventually() timeout failure.

samples keeps the first and last polls, with middle entries beyond the retention window counted in dropped, and total_polls is the real number of polls.

summary is a one-line trend classification of why the condition never held.

PollSample dataclass

PollSample(
    *,
    elapsed: float,
    outcome: str,
    value: object,
    detail: str,
    repeats: int = 1,
)

One recorded poll of an eventually() probe.

outcome is "fail" (the probe returned a value and the assertion on it failed) or "error" (the probe raised an ignored exception before producing a value).

value is a JSON-safe point-in-time snapshot of the probed value, None for "error" samples, and detail carries the failure message or the exception repr.

Consecutive identical polls are collapsed into one sample: repeats counts the run, and elapsed is its first occurrence, in seconds from the start of polling.

DanglingAssertionWarning

An assert_that() statement that asserts nothing.

Found at collection under --assertpy-dangling and emitted when the test that contains it is set up, so pytest attributes it to that test rather than to whichever ran first. A separate category so it can be turned into an error on its own with -W error::assertpy.DanglingAssertionWarning, the same way the vacuity and snapshot-key warnings are escalated.

VacuousAssertionWarning

Emitted when a universal assertion passes because there was nothing to check.

all_satisfy over an empty collection is true the way all([]) is true, so a query that returned no rows makes the assertion pass without examining anything. That is the most common silent false pass in a test suite, and the one a green run never reveals. Raise it to a failure with -W error::assertpy.VacuousAssertionWarning or silence it per call with allow_empty.

is_equal_to under ignore or include emits it too, when the filter left no key of the compared dict or object to compare.