Entry points¶
The top-level functions exported by assertpy.
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 assertpy 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 assertpy/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 assertpy/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.
The value is handed straight back under val and value, callable or not, and so is anything
else that is not callable, description among them.
Source code in assertpy/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 assertpy/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 in the models, dataclasses and TypedDict values the payload became inside
lists, tuples, sets, dicts, unions and root models, and reports the exact paths. It is alias-aware, and
respects a model that opts into extras (extra="allow"). A key that spells a field and was not read,
a name sent beside its alias, is dropped by pydantic like any other, and is named with where the field
was read from.
Raw items that no longer pair one by one with what they became are still checked where they can be:
- A set, a filtered list, an object wrapped into a list: validated again by their model class when they all became one, which runs that class's validators once more for them. JSON text is read as the original JSON input.
- An emptied list, whose raw items a validator left no built item for: checked against the type their field declares. They are validated again as it, which runs the item model's validators but not the field's own, so a field validator that would have built them otherwise is not seen.
What still cannot be paired fails with <path> cannot be checked and the reason:
- items of mixed classes in a set or a resized list
- emptied items the declared type does not build, or declares through a union of two or more types
besides
Noneor a named type alias - merged dict keys
- a generator consumed during validation, or a lazy
Iterable
It reads the payload as it was sent: what a validator changed in place in a plain dict or list is put back for the check and returned afterwards.
Two things it does not see:
- a validator that renames keys, or reorders or rewrites the items of a container without changing its size: those items are read by position
- a model inside a container of plain values (
list[Any])
A TypedDict is read off the schema pydantic built, so text annotations and type variables declare it
too. Where several are declared for one value and their Literal fields do not tell them apart, or a
type beside them may build a dict as well, a key the built dict lost fails with cannot be checked.
An exact failure carries its finds as a diff: an entry per undeclared field, holding the value the
payload sent there and the steps that lead to it, or one entry holding the part that cannot be checked
against the reason.
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 assertpy 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 assertpy/assertpy.py
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 997 998 999 1000 1001 1002 1003 1004 1005 1006 1007 1008 1009 1010 1011 1012 1013 1014 1015 1016 1017 1018 1019 1020 1021 1022 1023 1024 1025 1026 1027 1028 1029 1030 1031 1032 1033 1034 1035 1036 1037 1038 1039 1040 1041 1042 1043 1044 1045 1046 1047 1048 1049 1050 1051 1052 1053 1054 1055 1056 1057 1058 1059 | |
assert_warn ¶
assert_warn(
val: object,
description: str = "",
logger: WarningLogger | None = None,
*,
on_failure: Callable[[AssertionOutcome], object]
| None = None,
) -> Any
Set the value to be tested, and optional description and logger, and allow assertions to be
called, but never fail, only log warnings or hand each failure to on_failure.
This is a factory method for the AssertionBuilder, but unlike assert_that() a
failed assertion raises no AssertionError and does not halt execution. 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. An exception the logger or an on_failure callable raises does come out of the
assertion that failed, whatever its type.
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
|
on_failure
|
Callable
|
called with each failure's
|
None
|
Examples:
Usage:
from assertpy 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, each after the time it was logged, which is left out here:
WARNING [test.py:23]: Expected <foo> to be of length <4>, but was <3>.
WARNING [test.py:24]: Expected <foo> to be empty string, but was not.
WARNING [test.py:25]: Expected <False>, but was not.
WARNING [test.py:26]: Expected <foo> to contain only digits, but did not.
WARNING [test.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 assertpy/assertpy.py
1125 1126 1127 1128 1129 1130 1131 1132 1133 1134 1135 1136 1137 1138 1139 1140 1141 1142 1143 1144 1145 1146 1147 1148 1149 1150 1151 1152 1153 1154 1155 1156 1157 1158 1159 1160 1161 1162 1163 1164 1165 1166 1167 1168 1169 1170 1171 1172 1173 1174 1175 1176 1177 1178 1179 1180 1181 1182 1183 1184 1185 1186 1187 1188 1189 | |
WarningLoggingAdapter ¶
Logging adapter to unwind the stack to get the correct callee filename and line number.
log_passes ¶
log_passes(
logger: PassLogger | None = None,
*,
level: int = logging.INFO,
on_pass: Callable[[AssertionOutcome], object]
| None = None,
) -> contextlib.AbstractContextManager[None]
Report every assertion that passes inside the block, with the value it held for.
An assertion that holds says nothing by default. Inside this block each one logs a line naming the call, the value and the file and line it was asked at, and, for a comparison against a bound, how far the value was from it. Nothing else changes: a failure is raised, collected or logged exactly as it would be outside.
One line per assertion the test asked. What an assertion asks underneath (is_positive() asking
is_greater_than(0), an assertion inside a satisfies() predicate) is part of it and not reported again. A
soft or warn assertion that failed is not a pass, nor is one run under check(). A poll reports the steps of
the attempt that passed, once.
Uses contextvars, so each thread and task has its own: a task started inside the block reports into it while
the block is open. A builder reports into the block it was made in, while that block is open: one made before
it reports nothing, and one kept past its end reports nothing, in a later block too. A pivot reports where the
builder it came from reports.
Parameters:
| Name | Type | Description | Default |
|---|---|---|---|
logger
|
PassLogger | None
|
where the lines go, any object with |
None
|
level
|
int
|
the level of each line. Defaults to |
INFO
|
on_pass
|
Callable[[AssertionOutcome], object] | None
|
called with each pass's |
None
|
Examples:
Log the passes of a block:
from assertpy import assert_that, log_passes
with log_passes():
assert_that(latency).is_less_than(0.5)
assert_that(status).is_equal_to(200)
Each pass is one line at INFO on the assertpy.passes logger:
[test_api.py:5]: is_less_than(0.5) passed for <0.42>, the value is about 0.08 below the number given
[test_api.py:6]: is_equal_to(200) passed for <200>
Under pytest a passing test shows its log only when asked, so turn on live logging, for one run with
pytest -o log_cli=true --log-cli-level=INFO or for good in the configuration:
[tool.pytest.ini_options]
log_cli = true
log_cli_level = "INFO"
Raises:
| Type | Description |
|---|---|
TypeError
|
if |
Source code in assertpy/assertpy.py
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
soft-assertion state. A task started inside an open block collects into that block while it is open.
Examples:
Create a soft assertion context, and some failing tests:
from assertpy 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 assertpy/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 assertpy/assertpy.py
assert_all ¶
Run all callables inside a soft assertion context.
A convenience wrapper around soft_assertions() for inline use.
Examples:
Usage:
from assertpy 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 assertpy/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 assertpy 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 assertpy/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 assertpy 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 assertpy/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 assertpy 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 assertpy/assertpy.py
1277 1278 1279 1280 1281 1282 1283 1284 1285 1286 1287 1288 1289 1290 1291 1292 1293 1294 1295 1296 1297 1298 1299 1300 1301 1302 1303 1304 1305 1306 1307 1308 1309 1310 1311 1312 1313 1314 1315 1316 1317 1318 1319 1320 1321 1322 1323 1324 1325 1326 1327 1328 1329 1330 1331 1332 1333 1334 1335 1336 1337 1338 1339 1340 1341 1342 1343 1344 1345 1346 1347 1348 1349 1350 1351 1352 1353 1354 | |
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 assertpy import remove_extension
remove_extension(is_5)