Skip to content

Entry points

The top-level functions exported by assertpy2.

assert_that is statically overloaded: the return type narrows to the assertions valid for the value's type. Each per-type overload is listed below. See Type safety for how this drives editor autocomplete.

assert_that

assert_that(
    val: str, description: str = ""
) -> _StringAssertion
assert_that(
    val: bool, description: str = ""
) -> _BoolAssertion
assert_that(
    val: int, description: str = ""
) -> _NumericAssertion[int]
assert_that(
    val: float, description: str = ""
) -> _NumericAssertion[float]
assert_that(
    val: complex, description: str = ""
) -> _ComplexAssertion
assert_that(
    val: dict[_K, _V], description: str = ""
) -> _DictAssertion[_K, _V]
assert_that(
    val: list[_E] | tuple[_E, ...], description: str = ""
) -> _IterableAssertion[_E]
assert_that(
    val: set[_E] | frozenset[_E], description: str = ""
) -> _IterableAssertion[_E]
assert_that(
    val: date, description: str = ""
) -> _DateAssertion
assert_that(
    val: Path, description: str = ""
) -> _PathAssertion
assert_that(
    val: bytes, description: str = ""
) -> _BytesAssertion[bytes]
assert_that(
    val: bytearray, description: str = ""
) -> _BytesAssertion[bytearray]
assert_that(
    val: Callable[..., object], description: str = ""
) -> _CallableAssertion
assert_that(
    val: _T, description: str = ""
) -> AssertionBuilder[_T]

Set the value to be tested, plus an optional description, and allow assertions to be called.

This is a factory method for the AssertionBuilder, and the single most important method in all of assertpy.

Parameters:

Name Type Description Default
val object

the value to be tested (aka the actual value)

required
description str

the extra error message description. Defaults to '' (aka empty string)

''

Examples:

Just import it once at the top of your test file, and away you go...

from assertpy2 import assert_that

def test_something():
    assert_that(1 + 2).is_equal_to(3)
    assert_that('foobar').is_length(6).starts_with('foo').ends_with('bar')
    assert_that(['a', 'b', 'c']).contains('a').does_not_contain('x')
Source code in assertpy2/assertpy.py
def assert_that(val: object, description="") -> _CoreAssertion:
    """Set the value to be tested, plus an optional description, and allow assertions to be called.

    This is a factory method for the `AssertionBuilder`, and the single most important
    method in all of assertpy.

    Args:
        val: the value to be tested (aka the actual value)
        description (str, optional): the extra error message description. Defaults to ``''``
            (aka empty string)

    Examples:
        Just import it once at the top of your test file, and away you go...

            from assertpy2 import assert_that

            def test_something():
                assert_that(1 + 2).is_equal_to(3)
                assert_that('foobar').is_length(6).starts_with('foo').ends_with('bar')
                assert_that(['a', 'b', 'c']).contains('a').does_not_contain('x')
    """
    if _collecting() is not None:
        return _builder(val, description, "soft")
    return _builder(val, description)

value property

value: _T

The value under test, returned as-is for typed extract-and-continue.

Ends a chain by handing the checked value back, so a test can keep using it after the assertions passed. For object- and union-typed values the static type is refined by the narrowing assertions along the way: is_not_none() removes None and is_instance_of() narrows to the checked class, so no cast() or bare assert is needed to satisfy a type checker.

value is a strict-mode extraction: it hands the value back only when every assertion on it passed. If one failed under soft_assertions() or assert_warn() - where failures are collected, not raised - reading value raises TypeError instead of returning an unverified value. Read it in strict mode, or after the soft block has closed.

The taint is per-value, not per-chain. A value-changing pivot (extracting(), first(), decoded_as(), ...) starts a new value with a fresh guard and validates its own input, so a pivot never reaches .value with a value derived from a failed assertion - it raises in the pivot first.

Examples:

Usage:

order: Order | None = repo.find_order(42)
paid = assert_that(order).is_not_none().is_instance_of(PaidOrder).value
paid.refund()  # statically typed as PaidOrder

Returns:

Name Type Description
object _T

the original value under test (never a copy)

Raises:

Type Description
TypeError

if an assertion on this chain failed under soft_assertions() or assert_warn(), so the value cannot be trusted to match its narrowed type; the message carries the underlying (root) failure so its cause is not lost

check

check() -> CheckBuilder

Run the next assertion for its verdict instead of for its failure.

The assertion does not raise, collect or log. It returns an AssertionOutcome, truthy when it held and carrying the failure message, values and diff when it did not.

For asking a question about a value. An assertion states a requirement, and a test that stops at the first unmet one is the point; this is for the cases that are not that, like branching on a precondition, or reporting a check into a system that is not pytest.

A bad argument still raises. TypeError and ValueError mean the call itself is wrong, which is not a verdict about the value and would be silenced by returning one.

Examples:

Usage:

outcome = assert_that(response).check().is_equal_to(expected)
if not outcome:
    logger.warning(outcome.message)

assert_that(5).check().is_positive().passed        # True
assert_that(5).check().not_.is_positive().passed   # False
Source code in assertpy2/assertpy.py
def check(self) -> CheckBuilder:
    """Run the next assertion for its verdict instead of for its failure.

    The assertion does not raise, collect or log.  It returns an
    [`AssertionOutcome`][assertpy2.outcome.AssertionOutcome], truthy when it held and carrying the
    failure message, values and diff when it did not.

    For asking a question about a value.  An assertion states a requirement, and a test that stops
    at the first unmet one is the point; this is for the cases that are not that, like branching on
    a precondition, or reporting a check into a system that is not pytest.

    A bad argument still raises.  ``TypeError`` and ``ValueError`` mean the call itself is wrong,
    which is not a verdict about the value and would be silenced by returning one.

    Examples:
        Usage:

            outcome = assert_that(response).check().is_equal_to(expected)
            if not outcome:
                logger.warning(outcome.message)

            assert_that(5).check().is_positive().passed        # True
            assert_that(5).check().not_.is_positive().passed   # False
    """
    return CheckBuilder(self, self)

CheckBuilder

CheckBuilder(target: object, builder: AssertionBuilder)

Proxy returned by check().

Runs one assertion with the builder in verdict mode and hands back what it decided. The mode is put on and taken off around the call rather than held, so a builder that is also used normally afterwards is unaffected, and an assertion that raises for a bad argument still leaves it clean.

not_ is proxied rather than refused, so a negated assertion can be asked for a verdict too. Anything else that is not callable - val, description - is handed straight back.

Source code in assertpy2/assertpy.py
def __init__(self, target: object, builder: AssertionBuilder) -> None:
    # two references, because `not_` moves the target to the negation proxy while the mode and the
    # sink stay on the builder underneath it
    self._target = target
    self._builder = builder

AssertionOutcome dataclass

AssertionOutcome(
    *,
    passed: bool = False,
    message: str = "",
    actual: object = None,
    actual_provided: bool = False,
    expected: object = MISSING,
    diff: DiffResult | None = None,
    trace: PollTrace | None = None,
    group: str | None = None,
    location: tuple[str, int] | None = None,
    hint: str | None = None,
)

What one assertion decided, as a value rather than as a raised exception.

Returned by check(), which runs an assertion for its verdict instead of for its failure. Truthy when the assertion passed, so it reads as the answer to the question it was asked.

passed class-attribute instance-attribute

passed: bool = False

Whether the assertion held.

False on every record the failure path builds, which is all of them until something asks for a verdict: a failure is composed only when there is one.

message class-attribute instance-attribute

message: str = ''

The full failure text, description prefix and all, exactly as it reaches the reader.

Empty on a passing outcome. There is no message for an assertion that held, and inventing one would put words in the report of anything that logs whatever it is handed.

actual class-attribute instance-attribute

actual: object = None

The value under test. Filled from the builder when the assertion did not name one itself, so every failure carries it, and actual_provided says which of the two happened.

actual_provided class-attribute instance-attribute

actual_provided: bool = False

Whether the assertion passed actual itself rather than having it filled in.

Read by anything that renders: a value the assertion chose to name is worth showing, one filled in from the builder is usually already in the message.

trace class-attribute instance-attribute

trace: PollTrace | None = None

The convergence telemetry of a poll that timed out.

Here rather than only on the exception because a polling assertion under a soft block or in warn mode never builds one: it goes through the same delivery as everything else, and the trace used to stop at that boundary.

group class-attribute instance-attribute

group: str | None = None

The label a soft block was grouping under when this was collected.

Only a soft block groups, so this is None everywhere else, including on a failure that was raised. Kept on the record rather than beside it so a collected failure stays one thing.

location class-attribute instance-attribute

location: tuple[str, int] | None = None

The (file, line) of the caller, on a failure that was collected rather than raised.

None on a raised failure, whose traceback is the better answer, and where finding this costs a walk of the whole stack that nothing would read.

hint class-attribute instance-attribute

hint: str | None = None

The diagnostic line, kept apart from message as well as glued into it.

It is glued in because that is where a reader needs it, and kept apart because once it is part of the string nothing downstream can tell it from the assertion's own words.

has_expected property

has_expected: bool

Whether an expected value was named at all, which expected is not None cannot answer.

assert_conforms

assert_conforms(
    val: object,
    model: type[_U],
    description: str = ...,
    *,
    exact: bool = ...,
    each: Literal[False] = ...,
) -> AssertionBuilder[_U]
assert_conforms(
    val: object,
    model: type[_U],
    description: str = ...,
    *,
    exact: bool = ...,
    each: Literal[True],
) -> AssertionBuilder[list[_U]]

Validate val against a pydantic v2 model and continue over the validated instance.

The narrowing-complete companion to assert_that() for contract testing. Runs model.model_validate(val): on success the returned builder carries the validated, coerced instance (so .value and extracting see typed fields); on failure it fails with pydantic's validation errors.

Because the return type is driven by model rather than by the type of val, the chain narrows to model for any input - including the Any a decoded JSON payload carries.

With exact=True it also asserts contract drift: the payload must not carry fields the model does not declare. model_validate silently drops undeclared fields, so a stale model keeps passing after the live API grows new ones.

exact catches that drift - recursively, into nested sub-models and lists - and reports the exact paths. It is alias-aware, and respects a model that opts into extras (extra="allow").

Parameters:

Name Type Description Default
val object

the raw payload to validate (e.g. a decoded JSON response)

required
model type[_U]

a pydantic v2 model class (anything exposing model_validate)

required
description str

the extra error message description. Defaults to ''

''
exact bool

also fail if the payload carries fields model does not declare. Defaults to False

False
each bool

validate a list payload element-by-element against model (for list endpoints), narrowing the chain to list[model]. exact then applies per element. Defaults to False

False

Examples:

Usage:

from assertpy2 import assert_conforms, assert_that

order = assert_conforms(response.json(), OrderModel).value  # .value: OrderModel
assert_that(order.total).is_greater_than(0)

# catch silent API growth: fail if the response grew fields the model does not declare
assert_conforms(response.json(), OrderModel, exact=True)

# a list endpoint: validate every item, narrowing to list[OrderModel]
orders = assert_conforms(response.json(), OrderModel, each=True).value  # .value: list[OrderModel]

Returns:

Name Type Description
AssertionBuilder AssertionBuilder[Any]

a builder over the validated model instance, statically typed as model

Raises:

Type Description
TypeError

if model is not a pydantic v2 model class

AssertionError

if val does not validate against model, or (with exact) drifts from it

Source code in assertpy2/assertpy.py
def assert_conforms(
    val: object, model: type[_U], description: str = "", *, exact: bool = False, each: bool = False
) -> AssertionBuilder[Any]:
    """Validate ``val`` against a pydantic v2 ``model`` and continue over the validated instance.

    The narrowing-complete companion to [`assert_that()`][assertpy2.assertpy.assert_that] for
    contract testing.  Runs ``model.model_validate(val)``: on success the returned builder carries the
    validated, coerced instance (so ``.value`` and ``extracting`` see typed fields); on failure it
    fails with pydantic's validation errors.

    Because the return type is driven by ``model`` rather than by the type of ``val``, the chain
    narrows to ``model`` for **any** input - including the ``Any`` a decoded JSON payload carries.

    With ``exact=True`` it also asserts **contract drift**: the payload must not carry fields the model
    does not declare.  ``model_validate`` silently drops undeclared fields, so a stale model keeps
    passing after the live API grows new ones.

    ``exact`` catches that drift - recursively, into nested sub-models and lists - and reports the
    exact paths.  It is alias-aware, and respects a model that opts into extras (``extra="allow"``).

    Args:
        val: the raw payload to validate (e.g. a decoded JSON response)
        model: a pydantic v2 model class (anything exposing ``model_validate``)
        description (str, optional): the extra error message description.  Defaults to ``''``
        exact (bool, optional): also fail if the payload carries fields ``model`` does not declare.
            Defaults to ``False``
        each (bool, optional): validate a *list* payload element-by-element against ``model`` (for list
            endpoints), narrowing the chain to ``list[model]``.  ``exact`` then applies per element.
            Defaults to ``False``

    Examples:
        Usage:

            from assertpy2 import assert_conforms, assert_that

            order = assert_conforms(response.json(), OrderModel).value  # .value: OrderModel
            assert_that(order.total).is_greater_than(0)

            # catch silent API growth: fail if the response grew fields the model does not declare
            assert_conforms(response.json(), OrderModel, exact=True)

            # a list endpoint: validate every item, narrowing to list[OrderModel]
            orders = assert_conforms(response.json(), OrderModel, each=True).value  # .value: list[OrderModel]

    Returns:
        AssertionBuilder: a builder over the validated model instance, statically typed as ``model``

    Raises:
        TypeError: if ``model`` is not a pydantic v2 model class
        AssertionError: if ``val`` does not validate against ``model``, or (with ``exact``) drifts from it
    """
    if not (isinstance(model, type) and hasattr(model, "model_validate")):
        raise TypeError("assert_conforms requires a pydantic v2 model class")
    kind = "soft" if _collecting() is not None else None
    builder = _builder(val, description, kind)
    pydantic = sys.modules.get("pydantic")  # loaded already, since model exposes model_validate
    catchable: tuple[type[BaseException], ...] = (pydantic.ValidationError,) if pydantic is not None else ()
    if each:
        if not isinstance(val, (list, tuple)):
            raise TypeError("assert_conforms(each=True) requires a list or tuple payload")
        validated_items = []
        for index, item in enumerate(val):
            try:
                validated_items.append(model.model_validate(item))  # ty: ignore[call-non-callable]  # dynamic
            except catchable as exc:  # noqa: PERF203  # per-element catch reports which item failed; ~0 cost on 3.11+
                return builder.error(
                    f"Expected item [{index}] to conform to <{model.__name__}>, but it did not:\n{exc}",
                    actual=val,
                    expected=model,
                    diff=DiffResult(kind="match", entries=_contract_entries(exc, _ROOT.index(index))),
                    suppress_context=True,
                )
        if exact:
            drift = [f"[{index}].{path}" for index, item in enumerate(val) for path in contract_drift(item, model)]
            if drift:
                return builder.error(
                    f"Expected every item to conform exactly to <{model.__name__}>, but"
                    f" {len(drift)} undeclared field(s) the model does not declare: {sorted(drift)}",
                    actual=val,
                    expected=model,
                )
        return builder.builder(validated_items, description, kind)
    try:
        # duck-typed pydantic call, guarded by the hasattr check above
        validated = model.model_validate(val)  # ty: ignore[call-non-callable]  # model_validate is dynamic
    except catchable as exc:
        return builder.error(
            f"Expected <{_truncated(str(val))}> to conform to <{model.__name__}>, but it did not:\n{exc}",
            actual=val,
            expected=model,
            diff=DiffResult(kind="match", entries=_contract_entries(exc)),
            suppress_context=True,
        )
    if exact:
        drift = contract_drift(val, model)
        if drift:
            return builder.error(
                f"Expected <{_truncated(str(val))}> to conform exactly to <{model.__name__}>, but it carries"
                f" {len(drift)} undeclared field(s) the model does not declare: {sorted(drift)}",
                actual=val,
                expected=model,
            )
    return builder.builder(validated, description, kind)

assert_warn

assert_warn(val: object, description='', logger=None)

Set the value to be tested, and optional description and logger, and allow assertions to be called, but never fail, only log warnings.

This is a factory method for the AssertionBuilder, but unlike assert_that() an AssertionError is never raised, and execution is never halted. Instead, any assertion failures results in a warning message being logged. Uses the given logger, or defaults to a simple logger that prints warnings to stdout.

Parameters:

Name Type Description Default
val object

the value to be tested (aka the actual value)

required
description str

the extra error message description. Defaults to '' (aka empty string)

''
logger Logger

the logger for warning message on assertion failure. Defaults to None (aka use the default simple logger that prints warnings to stdout)

None

Examples:

Usage:

from assertpy2 import assert_warn

assert_warn('foo').is_length(4)
assert_warn('foo').is_empty()
assert_warn('foo').is_false()
assert_warn('foo').is_digit()
assert_warn('123').is_alpha()

Even though all of the above assertions fail, AssertionError is never raised and test execution is never halted. Instead, the failed assertions merely log the following warning messages to stdout:

2019-10-27 20:00:35 WARNING [test_foo.py:23]: Expected <foo> to be of length <4>, but was <3>.
2019-10-27 20:00:35 WARNING [test_foo.py:24]: Expected <foo> to be empty string, but was not.
2019-10-27 20:00:35 WARNING [test_foo.py:25]: Expected <False>, but was not.
2019-10-27 20:00:35 WARNING [test_foo.py:26]: Expected <foo> to contain only digits, but did not.
2019-10-27 20:00:35 WARNING [test_foo.py:27]: Expected <123> to contain only alphabetic chars, but did not.
Tip

Use assert_warn() if and only if you have a really good reason to log assertion failures instead of failing.

Source code in assertpy2/assertpy.py
def assert_warn(val: object, description="", logger=None):
    """Set the value to be tested, and optional description and logger, and allow assertions to be
    called, but never fail, only log warnings.

    This is a factory method for the `AssertionBuilder`, but unlike [`assert_that()`][assertpy2.assertpy.assert_that] an
    `AssertionError` is never raised, and execution is never halted.  Instead, any assertion failures
    results in a warning message being logged. Uses the given logger, or defaults to a simple logger
    that prints warnings to ``stdout``.


    Args:
        val: the value to be tested (aka the actual value)
        description (str, optional): the extra error message description. Defaults to ``''``
            (aka empty string)
        logger (Logger, optional): the logger for warning message on assertion failure. Defaults to ``None``
            (aka use the default simple logger that prints warnings to ``stdout``)

    Examples:
        Usage:

            from assertpy2 import assert_warn

            assert_warn('foo').is_length(4)
            assert_warn('foo').is_empty()
            assert_warn('foo').is_false()
            assert_warn('foo').is_digit()
            assert_warn('123').is_alpha()

        Even though all of the above assertions fail, ``AssertionError`` is never raised and
        test execution is never halted.  Instead, the failed assertions merely log the following
        warning messages to ``stdout``:

            2019-10-27 20:00:35 WARNING [test_foo.py:23]: Expected <foo> to be of length <4>, but was <3>.
            2019-10-27 20:00:35 WARNING [test_foo.py:24]: Expected <foo> to be empty string, but was not.
            2019-10-27 20:00:35 WARNING [test_foo.py:25]: Expected <False>, but was not.
            2019-10-27 20:00:35 WARNING [test_foo.py:26]: Expected <foo> to contain only digits, but did not.
            2019-10-27 20:00:35 WARNING [test_foo.py:27]: Expected <123> to contain only alphabetic chars, but did not.

    Tip:
        Use `assert_warn()` if and only if you have a *really* good reason to log assertion
        failures instead of failing.
    """
    return _builder(val, description, "warn", logger=logger)

soft_assertions

soft_assertions() -> _SoftAssertions

Create a soft assertion context.

Normally, any assertion failure will halt test execution immediately by raising an error. Soft assertions are way to collect assertion failures (and failure messages) together, to be raised all at once at the end, without halting your test.

Uses contextvars internally, so each thread and each asyncio task gets its own independent soft-assertion state.

Examples:

Create a soft assertion context, and some failing tests:

from assertpy2 import assert_that, soft_assertions

with soft_assertions():
    assert_that('foo').is_length(4)
    assert_that('foo').is_empty()
    assert_that('foo').is_false()
    assert_that('foo').is_digit()
    assert_that('123').is_alpha()

When the context ends, any assertion failures are collected together and a single AssertionError is raised, each tagged with the file:line it came from:

AssertionError: soft assertion failures:
1. Expected <foo> to be of length <4>, but was <3>.  [test_str.py:10]
2. Expected <foo> to be empty string, but was not.  [test_str.py:11]
3. Expected <False>, but was not.  [test_str.py:12]
4. Expected <foo> to contain only digits, but did not.  [test_str.py:13]
5. Expected <123> to contain only alphabetic chars, but did not.  [test_str.py:14]

Group errors by section:

with soft_assertions() as sa:
    with sa.group("Headers"):
        assert_that(headers["Content-Type"]).is_equal_to("application/json")
    with sa.group("Body"):
        assert_that(body["status"]).is_equal_to("ok")
Note

The soft assertion context only collects assertion failures, other errors such as TypeError or ValueError are always raised immediately. Triggering an explicit test failure with fail() will similarly halt execution immediately. If you need more forgiving behavior, use soft_fail() to add a failure message without halting test execution.

Source code in assertpy2/assertpy.py
def soft_assertions() -> _SoftAssertions:
    """Create a soft assertion context.

    Normally, any assertion failure will halt test execution immediately by raising an error.
    Soft assertions are way to collect assertion failures (and failure messages) together, to be
    raised all at once at the end, without halting your test.

    Uses `contextvars` internally, so each thread and each ``asyncio`` task gets its own
    independent soft-assertion state.

    Examples:
        Create a soft assertion context, and some failing tests:

            from assertpy2 import assert_that, soft_assertions

            with soft_assertions():
                assert_that('foo').is_length(4)
                assert_that('foo').is_empty()
                assert_that('foo').is_false()
                assert_that('foo').is_digit()
                assert_that('123').is_alpha()

        When the context ends, any assertion failures are collected together and a single
        ``AssertionError`` is raised, each tagged with the ``file:line`` it came from:

            AssertionError: soft assertion failures:
            1. Expected <foo> to be of length <4>, but was <3>.  [test_str.py:10]
            2. Expected <foo> to be empty string, but was not.  [test_str.py:11]
            3. Expected <False>, but was not.  [test_str.py:12]
            4. Expected <foo> to contain only digits, but did not.  [test_str.py:13]
            5. Expected <123> to contain only alphabetic chars, but did not.  [test_str.py:14]

        Group errors by section:

            with soft_assertions() as sa:
                with sa.group("Headers"):
                    assert_that(headers["Content-Type"]).is_equal_to("application/json")
                with sa.group("Body"):
                    assert_that(body["status"]).is_equal_to("ok")

    Note:
        The soft assertion context only collects *assertion* failures, other errors such as
        ``TypeError`` or ``ValueError`` are always raised immediately.  Triggering an explicit test
        failure with [`fail()`][assertpy2.assertpy.fail] will similarly halt execution immediately.
        If you need more forgiving behavior, use [`soft_fail()`][assertpy2.assertpy.soft_fail] to add
        a failure message without halting test execution.
    """
    return _SoftAssertions()

assert_all

assert_all(*callables: Callable[[], object]) -> None

Run all callables inside a soft assertion context.

A convenience wrapper around soft_assertions() for inline use.

Examples:

Usage:

from assertpy2 import assert_all, assert_that

assert_all(
    lambda: assert_that(x).is_positive(),
    lambda: assert_that(y).is_not_none(),
)

Raises:

Type Description
AssertionError

if any of the callables produce assertion failures

Source code in assertpy2/assertpy.py
def assert_all(*callables: Callable[[], object]) -> None:
    """Run all callables inside a soft assertion context.

    A convenience wrapper around [`soft_assertions()`][assertpy2.assertpy.soft_assertions] for inline use.

    Examples:
        Usage:

            from assertpy2 import assert_all, assert_that

            assert_all(
                lambda: assert_that(x).is_positive(),
                lambda: assert_that(y).is_not_none(),
            )

    Raises:
        AssertionError: if any of the callables produce assertion failures
    """
    with soft_assertions():
        for fn in callables:
            fn()

fail

fail(msg='')

Force immediate test failure with the given message.

Parameters:

Name Type Description Default
msg str

the failure message. Defaults to ''

''

Examples:

Fail a test:

from assertpy2 import assert_that, fail

def test_fail():
    fail('forced fail!')

If you wanted to test for a known failure, here is a useful pattern:

import operator

def test_adder_bad_arg():
    try:
        operator.add(1, 'bad arg')
        fail('should have raised error')
    except TypeError as e:
        assert_that(str(e)).contains('unsupported operand')
Source code in assertpy2/assertpy.py
def fail(msg=""):
    """Force immediate test failure with the given message.

    Args:
        msg (str, optional): the failure message.  Defaults to ``''``

    Examples:
        Fail a test:

            from assertpy2 import assert_that, fail

            def test_fail():
                fail('forced fail!')

        If you wanted to test for a known failure, here is a useful pattern:

            import operator

            def test_adder_bad_arg():
                try:
                    operator.add(1, 'bad arg')
                    fail('should have raised error')
                except TypeError as e:
                    assert_that(str(e)).contains('unsupported operand')
    """
    # no value under test and nothing compared, so the payload is empty. the class still matches every
    # other failure, so a handler written against the library does not have to name two of them
    raise AssertionFailure(f"Fail: {msg}!" if msg else "Fail!")

soft_fail

soft_fail(msg='')

Within a soft_assertions() context, append the failure message to the soft error list, but do not halt test execution.

Otherwise, outside the context, acts identical to fail() and forces immediate test failure with the given message.

Parameters:

Name Type Description Default
msg str

the failure message. Defaults to ''

''

Examples:

Failing soft assertions:

from assertpy2 import assert_that, soft_assertions, soft_fail

with soft_assertions():
    assert_that(1).is_equal_to(2)
    soft_fail('my message')
    assert_that('foo').is_equal_to('bar')

Fails, and outputs the following soft error list (each tagged with its file:line):

AssertionError: soft assertion failures:
1. Expected <1> to be equal to <2>, but was not.  [test_add.py:10]
2. Fail: my message!  [test_add.py:11]
3. Expected <foo> to be equal to <bar>, but was not.  [test_add.py:12]
Source code in assertpy2/assertpy.py
def soft_fail(msg=""):
    """Within a [`soft_assertions()`][assertpy2.assertpy.soft_assertions] context, append the failure
    message to the soft error list, but do not halt test execution.

    Otherwise, outside the context, acts identical to [`fail()`][assertpy2.assertpy.fail] and forces immediate test
    failure with the given message.

    Args:
        msg (str, optional): the failure message.  Defaults to ``''``

    Examples:
        Failing soft assertions:

            from assertpy2 import assert_that, soft_assertions, soft_fail

            with soft_assertions():
                assert_that(1).is_equal_to(2)
                soft_fail('my message')
                assert_that('foo').is_equal_to('bar')

        Fails, and outputs the following soft error list (each tagged with its ``file:line``):

            AssertionError: soft assertion failures:
            1. Expected <1> to be equal to <2>, but was not.  [test_add.py:10]
            2. Fail: my message!  [test_add.py:11]
            3. Expected <foo> to be equal to <bar>, but was not.  [test_add.py:12]

    """
    if (block := _collecting()) is not None:
        block.failures.append(
            AssertionOutcome(
                message=f"Fail: {msg}!" if msg else "Fail!",
                group=_soft_group.get(),
                location=_caller_location(),
            )
        )
        return
    fail(msg)

add_extension

add_extension(func, *, override: bool = False)

Add a new user-defined custom assertion to assertpy.

Once the assertion is registered with assertpy, use it like any other assertion. Pass val to assert_that(), and then call it.

A name already in use is refused, so an extension that would quietly replace a built-in assertion or another extension says so instead. Registering the same implementation again is not a clash: a module-scoped conftest fixture rebuilds its function on every module that requests it.

Parameters:

Name Type Description Default
func Callable

the assertion function (to be added)

required
override bool

replace an assertion of the same name instead of refusing

False

Raises:

Type Description
TypeError

if func is not callable

ValueError

if its __name__ is not an identifier, or the name is taken and override is false

Examples:

Usage:

from assertpy2 import add_extension

def is_5(self):
    if self.val != 5:
        return self.error(f'{self.val} is NOT 5!')
    return self

add_extension(is_5)

def test_5():
    assert_that(5).is_5()

def test_6():
    assert_that(6).is_5()  # fails
    # 6 is NOT 5!
Source code in assertpy2/assertpy.py
def add_extension(func, *, override: bool = False):
    """Add a new user-defined custom assertion to assertpy.

    Once the assertion is registered with assertpy, use it like any other assertion.  Pass val to
    [`assert_that()`][assertpy2.assertpy.assert_that], and then call it.

    A name already in use is refused, so an extension that would quietly replace a built-in assertion
    or another extension says so instead.  Registering the same implementation again is not a clash:
    a module-scoped ``conftest`` fixture rebuilds its function on every module that requests it.

    Args:
        func (Callable): the assertion function (to be added)
        override: replace an assertion of the same name instead of refusing

    Raises:
        TypeError: if ``func`` is not callable
        ValueError: if its ``__name__`` is not an identifier, or the name is taken and ``override``
            is false

    Examples:
        Usage:

            from assertpy2 import add_extension

            def is_5(self):
                if self.val != 5:
                    return self.error(f'{self.val} is NOT 5!')
                return self

            add_extension(is_5)

            def test_5():
                assert_that(5).is_5()

            def test_6():
                assert_that(6).is_5()  # fails
                # 6 is NOT 5!
    """
    if not callable(func):
        refuse(func, "callable", subject=argument("func"))
    name = getattr(func, "__name__", None)
    if not isinstance(name, str) or not name.isidentifier():
        raise ValueError(f"the assertion's __name__ must be a valid Python identifier, got {name!r}")
    with _extensions_lock:
        # re-adding the same implementation is a no-op, not a clash
        same = is_same_implementation(vars(_ExtendedBuilder).get(name), func) or is_same_implementation(
            _extensions.get(name), func
        )
        if not override and not same:
            # both of these used to go through in silence, and the second is the worse of the two:
            # an extension called `is_equal_to` replaced the core assertion, and every later call to
            # it failed with the extension's message instead
            if name in vars(_ExtendedBuilder) or name in _extensions:
                raise ValueError(
                    f"an assertion named {name!r} has already been added; pass override=True to "
                    f"replace it, or remove_extension() it first"
                )
            if hasattr(AssertionBuilder, name):
                raise ValueError(
                    f"{name!r} is already defined on the assertion builder; pass override=True to "
                    f"replace it deliberately, or give the extension another name"
                )
        if isinstance(func, types.FunctionType):
            # plain functions bind once here via the descriptor protocol, keeping assert_that() free of
            # per-call grafting; the dedicated subclass keeps AssertionBuilder itself pristine on removal
            setattr(_ExtendedBuilder, name, func)
        else:
            _extensions[name] = func

remove_extension

remove_extension(func)

Remove a user-defined custom assertion.

Parameters:

Name Type Description Default
func Callable

the assertion function (to be removed)

required

Examples:

Usage:

from assertpy2 import remove_extension

remove_extension(is_5)
Source code in assertpy2/assertpy.py
def remove_extension(func):
    """Remove a user-defined custom assertion.

    Args:
        func (Callable): the assertion function (to be removed)

    Examples:
        Usage:

            from assertpy2 import remove_extension

            remove_extension(is_5)
    """
    if not callable(func):
        refuse(func, "callable", subject=argument("func"))
    if func.__name__ in vars(_ExtendedBuilder):
        delattr(_ExtendedBuilder, func.__name__)
    _extensions.pop(func.__name__, None)