Skip to content

Snapshot assertions

Compare a value against a stored snapshot, recording it on first run.

Assertions that compare a value against a stored snapshot, recording it on the first run.

Capture a python data structure to disk as JSON and compare the current value against it on every later run - a few well-placed snapshot tests cover a lot of surface with very little code.

On-disk format

Snapshots are stored as readable JSON. For example:

assert_that({'a': 1, 'b': 2, 'c': 3}).snapshot()

is stored as:

{
    "a": 1,
    "b": 2,
    "c": 3
}

Most python structures are supported (dict, list, object, and so on). Values without a native JSON form round-trip through typed markers: set, complex, datetime/date/time (timezone-aware included), decimal.Decimal, bytes (base64-encoded), uuid.UUID, and Enum members. Any other type can be handled by registering a serializer with register_snapshot_serializer().

Updating

Run pytest with --assertpy2-snapshot-update (or set ASSERTPY2_SNAPSHOT_UPDATE for other runners) and every failing comparison overwrites the stored value instead of failing.

Each overwrite emits a SnapshotUpdatedWarning, and matching snapshots are left untouched. Deleting the snapshot files and re-running works too - each fresh capture emits a SnapshotCreatedWarning. So neither a first run nor an update run is ever silent, and both fail explicitly under -W error.

CI mode

On a first run a missing snapshot is created and the test passes. That is convenient locally, but a hazard in CI: a snapshot test whose golden was never committed would create it in the ephemeral workspace, pass, and silently disable drift detection for that test.

In CI mode a missing snapshot is instead a hard failure. Enable it with the --assertpy2-snapshot-ci pytest flag or the ASSERTPY2_SNAPSHOT_CI environment variable; it is also auto-enabled when a CI environment variable is set (the near-universal CI marker). Disable the autodetection with --assertpy2-snapshot-no-ci or ASSERTPY2_SNAPSHOT_CI=0. Local runs are unaffected.

snapshot

snapshot(
    id: str | None = None,
    path: str = "__snapshots",
    *,
    ignore: object = None,
    include: object = None,
    tolerance: float | None = None,
    comparators: dict | None = None,
    placeholders: dict | None = None,
) -> Self

Asserts that val is identical to the on-disk snapshot stored previously.

On the first run, before the snapshot file exists, the value is captured to disk, a SnapshotCreatedWarning is emitted, and the test passes. On every later run the value is compared to the stored snapshot and the test fails on any mismatch.

Snapshots live in the __snapshots directory by default (commit them to source control) and are identified by test filename plus line number, unless you pass id or path.

In update mode (--assertpy2-snapshot-update, or the ASSERTPY2_SNAPSHOT_UPDATE env var) a failing comparison overwrites the stored snapshot and passes, emitting a SnapshotUpdatedWarning; a matching snapshot is left untouched.

In CI mode (--assertpy2-snapshot-ci, ASSERTPY2_SNAPSHOT_CI, or an auto-detected CI environment) a missing snapshot is a hard AssertionError instead of being created, so an uncommitted golden fails the build rather than silently disabling drift detection.

The comparison accepts the same selective options as is_equal_to(), so volatile fields (timestamps, generated ids) or float noise don't break snapshots. The snapshot file always stores the full value; the options only shape the comparison.

Parameters:

Name Type Description Default
id str | None

a custom snapshot identifier (defaults to test filename plus line number)

None
path str

the directory where snapshots are stored (defaults to __snapshots)

'__snapshots'

Other Parameters:

Name Type Description
ignore Hashable | list | set | frozenset | None

the key/field (or collection of keys/fields) to ignore when comparing; accepts the same nested-path tuples, re.Pattern and type specs as is_equal_to().

include Hashable | list | set | frozenset | None

the key/field (or collection of keys/fields) to compare, everything else ignored.

tolerance float | None

an absolute tolerance applied to every real-number leaf.

comparators dict | None

a dict mapping a type or a field name to an (actual, expected) -> bool predicate that owns matching leaves.

placeholders dict | None

a dict mapping a top-level key of a dict-like val to a Matcher (or callable predicate). The stored snapshot records a descriptive token (Any<...>) for that field instead of the volatile value; each run then asserts the field is present and satisfies the matcher, not that it equals a fixed value - so a generated id or timestamp reads as its shape and never breaks the snapshot.

Examples:

Usage:

assert_that(None).snapshot()
assert_that(True).snapshot()
assert_that(1).snapshot()
assert_that(123.4).snapshot()
assert_that('foo').snapshot()
assert_that([1, 2, 3]).snapshot()
assert_that({'a': 1, 'b': 2, 'c': 3}).snapshot()
assert_that({'a', 'b', 'c'}).snapshot()
assert_that(1 + 2j).snapshot()
assert_that(someobj).snapshot()

By default, snapshots are identified by test filename plus line number. Alternately, you can specify a custom identifier using the id arg:

assert_that({'a': 1, 'b': 2, 'c': 3}).snapshot(id='foo-id')

By default, snapshots are stored in the __snapshots directory. Alternately, you can specify a custom path using the path arg:

assert_that({'a': 1, 'b': 2, 'c': 3}).snapshot(path='my-custom-folder')

Ignore volatile fields, or tolerate float noise, without touching the stored snapshot:

assert_that(api_response).snapshot(id='order', ignore=['created_at', ('user', 'session_id')])
assert_that(metrics).snapshot(id='latency', tolerance=0.001)

Store a shape token for a volatile field, and assert its shape on every run:

from assertpy2 import match

assert_that(response).snapshot(id='order', placeholders={'id': match.is_uuid()})
# stored as {"id": {"__placeholder__": "a valid UUID string"}, ...}

Returns:

Name Type Description
AssertionBuilder Self

returns this instance to chain to the next assertion

Raises:

Type Description
AssertionError

if val does not equal to on-disk snapshot

TypeError

if tolerance is not a real number, or comparators is not a dict of callables (validated on every run, including the capturing first one); or if placeholders is given for a non-dict-like val, or maps to a non-matcher value

ValueError

if tolerance is NaN or negative

Warns:

Type Description
SnapshotCreatedWarning

when this run captured a new snapshot instead of comparing

SnapshotUpdatedWarning

when update mode overwrote a stale snapshot instead of failing

Source code in assertpy2/snapshot.py
def snapshot(
    self,
    id: str | None = None,  # noqa: A002  # `id` is the public snapshot-identifier parameter
    path: str = "__snapshots",
    *,
    ignore: object = None,
    include: object = None,
    tolerance: float | None = None,
    comparators: dict | None = None,
    placeholders: dict | None = None,
) -> Self:
    """Asserts that val is identical to the on-disk snapshot stored previously.

    On the first run, before the snapshot file exists, the value is captured to disk, a
    [`SnapshotCreatedWarning`][assertpy2.snapshot.SnapshotCreatedWarning] is emitted, and the test
    passes.  On every later run the value is compared to the stored snapshot and the test fails on
    any mismatch.

    Snapshots live in the ``__snapshots`` directory by default (commit them to source control) and
    are identified by test filename plus line number, unless you pass ``id`` or ``path``.

    In **update mode** (``--assertpy2-snapshot-update``, or the ``ASSERTPY2_SNAPSHOT_UPDATE`` env
    var) a failing comparison overwrites the stored snapshot and passes, emitting a
    [`SnapshotUpdatedWarning`][assertpy2.snapshot.SnapshotUpdatedWarning]; a matching snapshot is
    left untouched.

    In **CI mode** (``--assertpy2-snapshot-ci``, ``ASSERTPY2_SNAPSHOT_CI``, or an auto-detected
    ``CI`` environment) a *missing* snapshot is a hard ``AssertionError`` instead of being created,
    so an uncommitted golden fails the build rather than silently disabling drift detection.

    The comparison accepts the same selective options as
    [`is_equal_to()`][assertpy2.base.BaseMixin.is_equal_to], so volatile fields (timestamps,
    generated ids) or float noise don't break snapshots.  The snapshot file always stores the
    **full** value; the options only shape the comparison.

    Args:
        id: a custom snapshot identifier (defaults to test filename plus line number)
        path: the directory where snapshots are stored (defaults to ``__snapshots``)

    Keyword Args:
        ignore (Hashable | list | set | frozenset | None): the key/field (or collection of
            keys/fields) to ignore when comparing; accepts the same nested-path tuples,
            ``re.Pattern`` and ``type`` specs as ``is_equal_to()``.
        include (Hashable | list | set | frozenset | None): the key/field (or collection of
            keys/fields) to compare, everything else ignored.
        tolerance (float | None): an absolute tolerance applied to every real-number leaf.
        comparators (dict | None): a dict mapping a ``type`` or a field name to an
            ``(actual, expected) -> bool`` predicate that owns matching leaves.
        placeholders (dict | None): a dict mapping a top-level key of a *dict-like* val to a
            ``Matcher`` (or callable predicate).  The stored snapshot records a descriptive token
            (``Any<...>``) for that field instead of the volatile value; each run then asserts the
            field is present and satisfies the matcher, not that it equals a fixed value - so a
            generated id or timestamp reads as its shape and never breaks the snapshot.

    Examples:
        Usage:

            assert_that(None).snapshot()
            assert_that(True).snapshot()
            assert_that(1).snapshot()
            assert_that(123.4).snapshot()
            assert_that('foo').snapshot()
            assert_that([1, 2, 3]).snapshot()
            assert_that({'a': 1, 'b': 2, 'c': 3}).snapshot()
            assert_that({'a', 'b', 'c'}).snapshot()
            assert_that(1 + 2j).snapshot()
            assert_that(someobj).snapshot()

        By default, snapshots are identified by test filename plus line number.
        Alternately, you can specify a custom identifier using the ``id`` arg:

            assert_that({'a': 1, 'b': 2, 'c': 3}).snapshot(id='foo-id')


        By default, snapshots are stored in the ``__snapshots`` directory.
        Alternately, you can specify a custom path using the ``path`` arg:

            assert_that({'a': 1, 'b': 2, 'c': 3}).snapshot(path='my-custom-folder')

        Ignore volatile fields, or tolerate float noise, without touching the stored snapshot:

            assert_that(api_response).snapshot(id='order', ignore=['created_at', ('user', 'session_id')])
            assert_that(metrics).snapshot(id='latency', tolerance=0.001)

        Store a shape token for a volatile field, and assert its shape on every run:

            from assertpy2 import match

            assert_that(response).snapshot(id='order', placeholders={'id': match.is_uuid()})
            # stored as {"id": {"__placeholder__": "a valid UUID string"}, ...}

    Returns:
        AssertionBuilder: returns this instance to chain to the next assertion

    Raises:
        AssertionError: if val does **not** equal to on-disk snapshot
        TypeError: if ``tolerance`` is not a real number, or ``comparators`` is not a dict of
            callables (validated on every run, including the capturing first one); or if
            ``placeholders`` is given for a non-dict-like val, or maps to a non-matcher value
        ValueError: if ``tolerance`` is ``NaN`` or negative

    Warns:
        SnapshotCreatedWarning: when this run captured a new snapshot instead of comparing
        SnapshotUpdatedWarning: when update mode overwrote a stale snapshot instead of failing
    """
    _build_compare_config(tolerance, comparators)  # a bad tolerance must fail the capturing first run too
    if placeholders:
        self._require_dict_like(self.val, name="val")  # placeholders address keys of a dict-like value
        for matcher in placeholders.values():
            if not _is_matcher(matcher) and not callable(matcher):
                raise TypeError("placeholder values must be Matcher instances or callables")
    # the stored snapshot documents placeholders as tokens; the comparison ignores those keys and
    # asserts their matcher separately, so a volatile field never breaks the snapshot
    stored_val = self._with_placeholder_tokens(placeholders) if placeholders else self.val
    effective_ignore = _combine_ignore(ignore, placeholders)
    lineno = ""
    if id:
        # custom id
        snapname = _name(path, id)
    else:
        # make id from filename and line number
        caller = _require_caller(inspect.currentframe())
        file_path = os.path.basename(caller.f_code.co_filename)
        file_name = os.path.splitext(file_path)[0]
        lineno = str(caller.f_lineno)
        snapname = _name(path, file_name)

    _record_access(snapname, "" if id else lineno, f"id={id!r}" if id else f"{file_path}:{lineno}")
    os.makedirs(path, exist_ok=True)

    # Serialize read-modify-write so parallel workers (pytest-xdist) sharing a snap file don't lose
    # each other's entries.  The normal comparison runs after the lock is released; the update-mode
    # rewrite decision must stay inside it.
    snapshot_value = _UNSET
    updated = False
    with _file_lock(snapname):
        if os.path.isfile(snapname):
            snap = _load(snapname)
            if id:
                # custom id, so test against the whole file
                snapshot_value = snap
            elif lineno in snap:
                # found sub-snap, so test
                snapshot_value = snap[lineno]
            else:
                # lineno not in snap, so create sub-snap and pass
                _forbid_creation_in_ci(snapname)
                snap[lineno] = stored_val
                _save(snapname, snap)

            if (
                snapshot_value is not _UNSET
                and _update_enabled()
                and self._snapshot_stale(
                    snapshot_value,
                    ignore=effective_ignore,
                    include=include,
                    tolerance=tolerance,
                    comparators=comparators,
                )
            ):
                if id:
                    _save(snapname, stored_val)
                else:
                    snap[lineno] = stored_val
                    _save(snapname, snap)
                updated = True
        else:
            # no snap, so create and pass
            _forbid_creation_in_ci(snapname)
            _save(snapname, stored_val if id else {lineno: stored_val})

    if updated:
        warnings.warn(
            f"updated snapshot <{snapname}>: this run overwrote the stored value instead of comparing;"
            " subsequent runs compare against it",
            SnapshotUpdatedWarning,
            stacklevel=2,
        )
        return self
    if snapshot_value is not _UNSET:
        if placeholders:
            self._check_placeholders(placeholders)
        try:
            return self.is_equal_to(
                snapshot_value,
                ignore=effective_ignore,
                include=include,
                tolerance=tolerance,
                comparators=comparators,
            )
        except AssertionFailure as mismatch:
            # name the snapshot the value was compared against: without it the failure is
            # indistinguishable from a plain is_equal_to, and the reader has no file to open
            # without a custom id the file holds one entry per line number, so the file alone does
            # not say which of them was compared
            located = snapname if id else f"{snapname}::{lineno}"
            raise _rewrapped(
                mismatch,
                f"{mismatch._message}{_shared_key_hint(snapname, '' if id else lineno)}"
                f" {_update_hint(f'Snapshot <{located}>', 'accept the new value')}",
            ) from None
    warnings.warn(
        f"created snapshot <{snapname}>: this run captured the value instead of comparing;"
        " subsequent runs compare against it (delete the file to re-capture)",
        SnapshotCreatedWarning,
        stacklevel=2,
    )
    return self

matches_inline

matches_inline(
    expected: object = _UNSET,
    *,
    ignore: object = None,
    include: object = None,
    tolerance: float | None = None,
    comparators: dict[object, Callable[..., bool]]
    | None = None,
    placeholders: dict[object, object] | None = None,
) -> Self

Asserts that val equals an inline snapshot literal written at the call site.

Unlike snapshot(), which stores the value in a separate file, an inline snapshot lives as a literal argument in the test source.

Call it empty the first time and run with --assertpy2-snapshot-update to record the value into the source; later runs compare against it. The same selective knobs as snapshot() apply, so volatile fields never make the snapshot brittle.

The comparison itself is an ordinary equality check with no source introspection, so it works under pytest-xdist and needs neither the [inline] extra nor any assertion rewriting; only recording (empty call under update mode) reads the source.

Parameters:

Name Type Description Default
expected object

the recorded literal; omit it to record on the next update run.

_UNSET
ignore object

key(s)/path(s) to skip in the comparison (as in is_equal_to).

None
include object

restrict the comparison to these key(s)/path(s).

None
tolerance float | None

absolute numeric tolerance applied at every depth.

None
comparators dict[object, Callable[..., bool]] | None

per-type custom equality callables.

None
placeholders dict[object, object] | None

{key: matcher} for volatile fields - the key is ignored by the equality comparison and its matcher asserted separately.

None

Examples:

Usage:

assert_that({"id": 1, "name": "Alice"}).matches_inline({"id": 1, "name": "Alice"})

Returns:

Name Type Description
AssertionBuilder Self

returns this instance to chain to the next assertion

Raises:

Type Description
AssertionError

if val does not equal the recorded literal, or the snapshot is still empty

Source code in assertpy2/snapshot.py
def matches_inline(
    self,
    expected: object = _UNSET,
    *,
    ignore: object = None,
    include: object = None,
    tolerance: float | None = None,
    comparators: dict[object, Callable[..., bool]] | None = None,
    placeholders: dict[object, object] | None = None,
) -> Self:
    """Asserts that val equals an inline snapshot literal written at the call site.

    Unlike [`snapshot()`][assertpy2.snapshot.SnapshotMixin.snapshot], which stores the value in a
    separate file, an inline snapshot lives as a literal argument in the test source.

    Call it empty the first time and run with ``--assertpy2-snapshot-update`` to record the value
    into the source; later runs compare against it. The same selective knobs as ``snapshot()``
    apply, so volatile fields never make the snapshot brittle.

    The comparison itself is an ordinary equality check with no source introspection, so it works
    under ``pytest-xdist`` and needs neither the ``[inline]`` extra nor any assertion rewriting;
    only recording (empty call under update mode) reads the source.

    Args:
        expected: the recorded literal; omit it to record on the next update run.
        ignore: key(s)/path(s) to skip in the comparison (as in ``is_equal_to``).
        include: restrict the comparison to these key(s)/path(s).
        tolerance: absolute numeric tolerance applied at every depth.
        comparators: per-type custom equality callables.
        placeholders: ``{key: matcher}`` for volatile fields - the key is ignored by the equality
            comparison and its matcher asserted separately.

    Examples:
        Usage:

            assert_that({"id": 1, "name": "Alice"}).matches_inline({"id": 1, "name": "Alice"})

    Returns:
        AssertionBuilder: returns this instance to chain to the next assertion

    Raises:
        AssertionError: if val does not equal the recorded literal, or the snapshot is still empty
    """
    if placeholders:
        self._require_dict_like(self.val, name="val")  # placeholders address keys of a dict-like value
        for matcher in placeholders.values():
            if not _is_matcher(matcher) and not callable(matcher):
                raise TypeError("placeholder values must be Matcher instances or callables")
    if expected is _UNSET:
        if _ci_mode_enabled():
            raise AssertionError(
                "inline snapshot is empty and CI mode forbids recording it - record it locally with"
                " --assertpy2-snapshot-update and commit the source"
            )
        if _update_enabled():
            _inline_literal_or_raise(self.val)
            caller = _require_caller(inspect.currentframe())
            _inline.record_create(caller, self.val)
            warnings.warn(
                "recorded inline snapshot: this run captured the value into the test source;"
                " subsequent runs compare against it",
                SnapshotCreatedWarning,
                stacklevel=2,
            )
            return self
        raise AssertionError("inline snapshot is empty; run --assertpy2-snapshot-update to record it")

    effective_ignore = _combine_ignore(ignore, placeholders)
    if _update_enabled() and self._snapshot_stale(
        expected, ignore=effective_ignore, include=include, tolerance=tolerance, comparators=comparators
    ):
        _inline_literal_or_raise(self.val)
        caller = _require_caller(inspect.currentframe())
        _inline.record_update(caller, self.val)
        warnings.warn(
            "updated inline snapshot: this run overwrote the stored literal instead of comparing;"
            " subsequent runs compare against it",
            SnapshotUpdatedWarning,
            stacklevel=2,
        )
        return self
    if placeholders:
        self._check_placeholders(placeholders)
    try:
        return self.is_equal_to(
            expected, ignore=effective_ignore, include=include, tolerance=tolerance, comparators=comparators
        )
    except AssertionFailure as mismatch:
        # the stored snapshot lives in this very call, and updating rewrites it in place: saying so
        # is what the file-backed branch already does for its own kind
        raise _rewrapped(
            mismatch, f"{mismatch._message} {_update_hint('Inline snapshot', 'rewrite the literal here')}"
        ) from None

matches_contract_snapshot

matches_contract_snapshot(
    id: str | None = None, path: str = "__snapshots"
) -> Self

Asserts that val's structure matches a contract snapshot stored previously.

Records the shape - paths and type categories, never values - on the first run, then on later runs fails only on structural drift: a field added, removed, or retyped.

It is value-tolerant by construction, so dynamic ids, timestamps, and amounts change freely without breaking the snapshot, and it needs no hand-written model - the contract is inferred from the first response. Numbers are one category (5 and 5.0 do not drift), and a null sample is a nullable wildcard.

The model-driven counterpart is assert_conforms(..., exact=True): reach for that when you already have a pydantic model, and for this when you would rather capture the shape from a real response.

Honors the same update mode (--assertpy2-snapshot-update), CI mode (--assertpy2-snapshot-ci), and storage layout as snapshot().

Because a contract is inferred from a single observation it cannot know which fields are optional, so a legitimately sometimes-absent field reads as removed; re-record with update mode when the contract really changed.

Parameters:

Name Type Description Default
id str | None

a custom snapshot identifier (defaults to test filename plus line number)

None
path str

the directory where snapshots are stored (defaults to __snapshots)

'__snapshots'

Examples:

Usage:

assert_that(response.json()).matches_contract_snapshot()

Returns:

Name Type Description
AssertionBuilder Self

returns this instance to chain to the next assertion

Raises:

Type Description
AssertionError

if val's structure drifts from the stored contract snapshot

Warns:

Type Description
SnapshotCreatedWarning

when this run captured a new contract instead of comparing

SnapshotUpdatedWarning

when update mode overwrote a drifted contract instead of failing

Source code in assertpy2/snapshot.py
def matches_contract_snapshot(self, id: str | None = None, path: str = "__snapshots") -> Self:  # noqa: A002  # `id` is the public snapshot-identifier parameter
    """Asserts that val's *structure* matches a contract snapshot stored previously.

    Records the shape - paths and type categories, never values - on the first run, then on later
    runs fails only on **structural** drift: a field added, removed, or retyped.

    It is value-tolerant by construction, so dynamic ids, timestamps, and amounts change freely
    without breaking the snapshot, and it needs no hand-written model - the contract is inferred
    from the first response.  Numbers are one category (``5`` and ``5.0`` do not drift), and a
    ``null`` sample is a nullable wildcard.

    The model-driven counterpart is
    [`assert_conforms(..., exact=True)`][assertpy2.assertpy.assert_conforms]: reach for that when you
    already have a pydantic model, and for this when you would rather capture the shape from a real
    response.

    Honors the same update mode (``--assertpy2-snapshot-update``), CI mode
    (``--assertpy2-snapshot-ci``), and storage layout as
    [`snapshot()`][assertpy2.snapshot.SnapshotMixin.snapshot].

    Because a contract is inferred from a single observation it cannot know which fields are
    optional, so a legitimately sometimes-absent field reads as ``removed``; re-record with update
    mode when the contract really changed.

    Args:
        id: a custom snapshot identifier (defaults to test filename plus line number)
        path: the directory where snapshots are stored (defaults to ``__snapshots``)

    Examples:
        Usage:

            assert_that(response.json()).matches_contract_snapshot()

    Returns:
        AssertionBuilder: returns this instance to chain to the next assertion

    Raises:
        AssertionError: if val's structure drifts from the stored contract snapshot

    Warns:
        SnapshotCreatedWarning: when this run captured a new contract instead of comparing
        SnapshotUpdatedWarning: when update mode overwrote a drifted contract instead of failing
    """
    contract = shape(self.val)
    lineno = ""
    if id:
        snapname = _name(path, id)
    else:
        caller = _require_caller(inspect.currentframe())
        file_path = os.path.basename(caller.f_code.co_filename)
        file_name = os.path.splitext(file_path)[0]
        lineno = str(caller.f_lineno)
        snapname = _name(path, file_name)

    _record_access(snapname, "" if id else lineno, f"id={id!r}" if id else f"{file_path}:{lineno}")
    os.makedirs(path, exist_ok=True)

    stored = _UNSET
    updated = False
    with _file_lock(snapname):
        if os.path.isfile(snapname):
            snap = _load(snapname)
            if id:
                stored = snap
            elif lineno in snap:
                stored = snap[lineno]
            else:
                _forbid_creation_in_ci(snapname)
                snap[lineno] = contract
                _save(snapname, snap)

            if stored is not _UNSET and _update_enabled() and shape_diff(stored, contract):
                if id:
                    _save(snapname, contract)
                else:
                    snap[lineno] = contract
                    _save(snapname, snap)
                updated = True
        else:
            _forbid_creation_in_ci(snapname)
            _save(snapname, contract if id else {lineno: contract})

    if updated:
        warnings.warn(
            f"updated contract snapshot <{snapname}>: this run overwrote the stored shape instead of"
            " comparing; subsequent runs compare against it",
            SnapshotUpdatedWarning,
            stacklevel=2,
        )
        return self
    if stored is not _UNSET:
        drift = shape_diff(stored, contract)
        if drift:
            located = snapname if id else f"{snapname}::{lineno}"
            return self.error(
                f"Expected <{_truncated(str(self.val))}> to match contract snapshot, but the structure"
                f" drifted:\n{_format_shape_drift(drift)}\n"
                f"{_update_hint(f'Contract snapshot <{located}>', 'accept the new shape')}",
                actual=self.val,
            )
        return self
    warnings.warn(
        f"created contract snapshot <{snapname}>: this run captured the shape instead of comparing;"
        " subsequent runs compare against it (delete the file to re-capture)",
        SnapshotCreatedWarning,
        stacklevel=2,
    )
    return self

register_snapshot_serializer

register_snapshot_serializer(
    cls: type,
    encode: Callable[[Any], object],
    decode: Callable[[Any], object],
    *,
    tag: str | None = None,
) -> None

Register a custom (encode, decode) pair for snapshotting values of type cls.

The typed codec covers common non-JSON types (set, complex, datetime/date/time, Decimal, bytes, uuid.UUID, Enum). Register a serializer for anything else - a domain object, an ORM row, a pathlib.Path - so snapshot() stores and round-trips it instead of raising TypeError.

Matching is by isinstance (so subclasses are covered), the registry is consulted before the built-ins, and a later registration wins over an earlier one for overlapping types.

Parameters:

Name Type Description Default
cls type

the type (matched by isinstance) the serializer applies to

required
encode Callable[[Any], object]

value -> json_safe (the returned object must itself be JSON-serializable or handled by another serializer)

required
decode Callable[[Any], object]

json_safe -> value (the inverse; runs your own code on snapshot load, so it is a trusted, explicit opt-in - unlike the automatic instance decode, which never imports)

required
tag str | None

a stable identifier stored in the snapshot to route decoding (defaults to the type's fully-qualified name); change it only deliberately, since existing snapshots key on it

None

Examples:

Usage:

import pathlib

register_snapshot_serializer(pathlib.PurePath, str, pathlib.PurePath)

Raises:

Type Description
TypeError

if cls is not a type, or encode / decode are not callable

Source code in assertpy2/snapshot.py
def register_snapshot_serializer(
    cls: type,
    # `Any`, not `object`: a codec is only ever handed an instance of `cls`, and its decode only the
    # payload its own encode produced. Demanding a parameter typed `object` rejects correct codecs,
    # `pathlib.PurePath` as a decode among them, which is the pair recommended below
    encode: Callable[[Any], object],
    decode: Callable[[Any], object],
    *,
    tag: str | None = None,
) -> None:
    """Register a custom (encode, decode) pair for snapshotting values of type ``cls``.

    The typed codec covers common non-JSON types (``set``, ``complex``, ``datetime``/``date``/``time``,
    ``Decimal``, ``bytes``, ``uuid.UUID``, ``Enum``).  Register a serializer for anything else - a
    domain object, an ORM row, a ``pathlib.Path`` - so ``snapshot()`` stores and round-trips it instead
    of raising ``TypeError``.

    Matching is by ``isinstance`` (so subclasses are covered), the registry is consulted **before** the
    built-ins, and a later registration wins over an earlier one for overlapping types.

    Args:
        cls: the type (matched by ``isinstance``) the serializer applies to
        encode: ``value -> json_safe`` (the returned object must itself be JSON-serializable or
            handled by another serializer)
        decode: ``json_safe -> value`` (the inverse; runs your own code on snapshot load, so it is a
            trusted, explicit opt-in - unlike the automatic instance decode, which never imports)
        tag: a stable identifier stored in the snapshot to route decoding (defaults to the type's
            fully-qualified name); change it only deliberately, since existing snapshots key on it

    Examples:
        Usage:

            import pathlib

            register_snapshot_serializer(pathlib.PurePath, str, pathlib.PurePath)

    Raises:
        TypeError: if ``cls`` is not a type, or ``encode`` / ``decode`` are not callable
    """
    if not isinstance(cls, type):
        raise TypeError("cls must be a type")
    if not callable(encode) or not callable(decode):
        raise TypeError("encode and decode must be callable")
    _SERIALIZERS.insert(0, _Serializer(cls, encode, decode, tag or f"{cls.__module__}.{cls.__qualname__}"))

SnapshotKeyReusedWarning

Emitted when one snapshot key was reached by more than one test.

The default key is the line of the snapshot() call, so every case of a parametrised test shares it: the first case stores its value and the rest compare against that one. Where the values differ the run fails with a message that names two unrelated cases, and where they agree it passes while checking one case out of however many - the second is the reason this warning exists, because nothing else reports it.

Two calls inside one test are not this: a helper that snapshots twice asserts both values, so the metric is distinct tests rather than accesses.

Its own category, so it can be raised to an error with a single filterwarnings entry.

SnapshotCreatedWarning

Emitted when snapshot() writes a new snapshot instead of comparing.

The first run of a snapshot assertion captures the current value and passes without comparing anything, so a wrong first capture would silently become the reference. This warning makes that capture visible; suites running with -W error turn it into an explicit failure.

SnapshotUpdatedWarning

Emitted when snapshot() overwrites a stored snapshot in update mode.

Update mode replaces failing snapshots with the current value instead of failing; turn it on with the --assertpy2-snapshot-update pytest flag or the ASSERTPY2_SNAPSHOT_UPDATE environment variable.

Each overwrite emits this warning, so an update run reports exactly which snapshots changed instead of rewriting them silently.