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 assertpy2/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._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']

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.

DanglingAssertionWarning

An assert_that() statement that asserts nothing.

Found at collection under --assertpy2-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::assertpy2.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::assertpy2.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.