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,
    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 assertpy2/errors.py
def __init__(
    self,
    message: str,
    *,
    actual: object = None,
    expected: object = None,
    diff: DiffResult | None = None,
    trace: PollTrace | None = None,
    failures: tuple[AssertionOutcome, ...] = (),
):
    super().__init__(message)
    self._message = message
    self.actual = actual
    self.expected = expected
    self.diff = diff
    self.trace = trace
    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._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.  Stays ``None`` on a failure built directly,
    which `eventually()` and the snapshot re-wraps still do.

    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.
    """

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.

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']

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, which has no position to index by. line is the 1-based line number of a text or bytes diff.

value instance-attribute

value: object

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

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.