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 ¶
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 |
''
|
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
value
property
¶
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 |
check ¶
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.
An assertion or an extension that fails more than once answers with its first failure, the one
it raises outside check().
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
CheckBuilder ¶
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
NegatedBuilder ¶
Proxy that inverts the next assertion. Created by assert_that(val).not_.
Generic over the assertion type it was reached from, so inverting a step returns the view the chain
had before .not_: assert_that("x").not_.starts_with("y") stays a string assertion rather
than collapsing to the untyped builder and letting a numeric assertion follow it, and a negated
narrowing narrows nothing.
Source code in assertpy2/assertpy.py
AssertionOutcome
dataclass
¶
AssertionOutcome(
*,
passed: bool = False,
message: str = "",
actual: object = None,
actual_provided: bool = False,
expected: object = MISSING,
diff: DiffResult | None = None,
requirement: Requirement | 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
¶
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
¶
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
¶
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
¶
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.
requirement
class-attribute
instance-attribute
¶
Which operation was asked, with which parameters, and whether it was negated.
None where no operation was asked, and that is a limit of the contract rather than a gap to be
closed later. Three shapes have no operation to name: fail(), a bare error() carrying a
message of the caller's own, and a precondition of one of the few members _operations.py records
as asserting nothing, where the failure is about the value's shape and not about a requirement.
trace
class-attribute
instance-attribute
¶
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
¶
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
¶
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
¶
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
¶
Whether an expected value was named at all, which expected is not None cannot answer.
Requirement
dataclass
¶
What was asked of the value, as data rather than as a sentence.
A consumer reading AssertionOutcome could tell what failed only by parsing the message: the
fields beside it answer what the values were, not what was asked of them. expected carries five
different things depending on the family, the operand for is_equal_to(2), a tuple for
contains(2), a rendered description for satisfies(...), a type for is_instance_of(str), and
nothing at all for is_empty(), so grouping failures across a suite meant reading English.
operation
instance-attribute
¶
The name the caller wrote, as it appears in the API.
A delegating assertion answers its own name rather than the one it delegates to: is_positive()
asks is_greater_than(0) underneath, and the reader wants the assertion in the test.
parameters
class-attribute
instance-attribute
¶
Every declared parameter with the value the assertion ran with, by name.
Bound values rather than the call as written, which means a parameter left out appears with its
default. Two spellings of one requirement then group as one: is_close_to(9, 0.1) and
is_close_to(other=9, tolerance=0.1) both read {"other": 9, "tolerance": 0.1}, where a
positional tuple would also be indistinguishable from a two-operand contains.
The values are the objects themselves, as actual and expected already are, so a consumer that
serialises has the same work to do here as there.
negated
class-attribute
instance-attribute
¶
Whether the assertion ran through not_, where holding is the failure.
assert_conforms ¶
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 |
required |
description
|
str
|
the extra error message description. Defaults to |
''
|
exact
|
bool
|
also fail if the payload carries fields |
False
|
each
|
bool
|
validate a list payload element-by-element against |
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 |
Raises:
| Type | Description |
|---|---|
TypeError
|
if |
AssertionError
|
if |
Source code in assertpy2/assertpy.py
687 688 689 690 691 692 693 694 695 696 697 698 699 700 701 702 703 704 705 706 707 708 709 710 711 712 713 714 715 716 717 718 719 720 721 722 723 724 725 726 727 728 729 730 731 732 733 734 735 736 737 738 739 740 741 742 743 744 745 746 747 748 749 750 751 752 753 754 755 756 757 758 759 760 761 762 763 764 765 766 767 768 769 770 771 772 773 774 775 776 777 778 779 780 781 782 783 784 785 786 787 788 | |
assert_warn ¶
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 |
''
|
logger
|
Logger
|
the logger for warning message on assertion failure. Defaults to |
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
WarningLoggingAdapter ¶
Logging adapter to unwind the stack to get the correct callee filename and line number.
soft_assertions ¶
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
SoftAssertionCollector ¶
Collector returned by soft_assertions() for grouping
errors hierarchically.
group ¶
Group subsequent assertion failures under label.
Examples:
Usage:
with soft_assertions() as sa:
with sa.group("Headers"):
assert_that(headers).contains_key("Content-Type")
with sa.group("Body"):
assert_that(body["status"]).is_equal_to("ok")
Source code in assertpy2/assertpy.py
assert_all ¶
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 |
TypeError
|
if a callable hands back a coroutine, which has asserted nothing |
Source code in assertpy2/assertpy.py
fail ¶
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
soft_fail ¶
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
add_extension ¶
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 |
ValueError
|
if its |
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
919 920 921 922 923 924 925 926 927 928 929 930 931 932 933 934 935 936 937 938 939 940 941 942 943 944 945 946 947 948 949 950 951 952 953 954 955 956 957 958 959 960 961 962 963 964 965 966 967 968 969 970 971 972 973 974 975 976 977 978 979 980 981 982 983 984 985 986 987 988 989 990 991 992 993 994 995 996 | |
remove_extension ¶
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)