The fully-typed fluent assertion library for Python
A modern, batteries-included fork of assertpy
pip install assertpy2 # drop-in replacement for assertpy, just change the importfrom assertpy2 import assert_that
def test_user():
user = {"name": "Alice", "age": 30, "roles": ["viewer", "editor"]}
assert_that(user).contains_key("name", "age")
assert_that(user["age"]).is_between(18, 120)
assert_that(user["roles"]).contains("viewer").does_not_contain("admin")
assert_that(user).has_name("Alice")Browse the full documentation for every assertion, matcher, and integration.
A fluent chain reads as one intent and replaces several bare asserts - and your IDE offers only the methods that fit the value's type:
# bare - three statements, no autocomplete help
assert isinstance(items, list)
assert len(items) == 3
assert "admin" in items
# assertpy2 - one chain, type-aware autocomplete
assert_that(items).is_instance_of(list).is_length(3).contains("admin")The real difference shows up when a test fails. Here a nested response has two wrong
fields. Plain assert dumps both structures and leaves you to find them:
assert response == expected
E AssertionError: assert {'id': 1, ...} == {'id': 1, ...}
E Omitting 1 identical items, use -vv to show
E Differing items:
E {'user': {'name': 'Alice', 'role': 'superadmin'}} != {'user': {'name': 'Alice', 'role': 'admin'}}
E {'status': 'active'} != {'status': 'disabled'}
assertpy2 reports the exact path to every difference, in color:
assert_that(response).is_equal_to(expected)Recursive diffs cover dicts, dataclasses, namedtuples, and Pydantic models - and lists, sets, and matcher predicates get the same path-level treatment. For dynamic fields (IDs, timestamps), validate a subset with matches_structure().
assert_that() uses @overload to return type-specific Protocols.
Your IDE shows only methods relevant to the value you're testing, not all 100+:
assert_that("hello").→ string methods:starts_with,matches,is_alpha, ...assert_that(42).→ numeric methods:is_positive,is_between,is_close_to, ...assert_that(Path("/tmp")).→ path methods:exists,is_file,is_readable, ...assert_that(my_dict).→ dict methods:contains_key,contains_entry,has_json_path, ...assert_that(b"\x89PNG").→ bytes methods:starts_with_bytes,is_valid_utf8,decoded_as, ...
9 type-specific Protocols instead of one Any. Works in PyCharm, VS Code, and any LSP-compatible editor.
An assertion doesn't just check a value - it hands it back, statically narrowed. is_not_none() strips
None, is_instance_of() narrows to the class, and .value returns the result with no cast and no
bare assert:
order = assert_that(repo.find(42)).is_not_none().is_instance_of(PaidOrder).value
order.refund() # statically PaidOrder - verified by ty, mypy, and pyrightFor API tests, assert_conforms() validates a raw payload against a Pydantic model and narrows the chain to it, with exact=True catching silent contract drift:
data = assert_conforms(response.json(), OrderModel).value # data: OrderModelReturning the value it verified, statically narrowed, lets you assert and use the result in a single step.
Fluent API
- Composable matchers:
match.greater_than(5),match.is_uuid(), combine with&,|,~. Also work with plainassert ==. - Structural matching:
matches_structure()for declarative dict/API-response validation, reporting the exact path on failure. - Recursive field assertions:
all_fields_satisfy()/has_no_none_fields()apply a predicate to every leaf of an object graph, reporting the exact path. - Universal negation:
.not_inverts any assertion without dedicatedis_not_*methods. - Collection pipeline:
filtered_on(),mapped(),flat_mapped(),first(),last(),element(),single(). - Positional & pairwise checks:
satisfies_exactly(),zip_satisfies(),contains_only_once(),has_same_size_as(), plus*_in_any_ordervariants. - Fluent chaining: write assertions as readable one-liners that chain naturally.
Type safety
- Type-aware autocomplete: 9 Protocols, IDE shows only relevant methods per type.
- Typed narrowing:
.valuehands the checked value back;is_not_none(),is_instance_of(), and asatisfies()TypeIspredicate narrow its static type - no casts. - Contract testing:
assert_conforms()validates a raw payload against a Pydantic model and narrows the chain to it - the capstone for API-response tests;exact=Truecatches silent contract drift (undeclared fields),each=Truevalidates list endpoints. - py.typed:
Selfreturn types, PEP 561 compliant (PEP 561).
Built-in types
- Strings, numbers, lists, tuples, sets, dicts, dates, booleans, objects, bytes, files, exceptions.
- Bytes assertions:
is_valid_utf8(),starts_with_bytes(),is_hex_equal_to(),decoded_as()forbytes/bytearray. - Dynamic assertions:
has_<name>()for any attribute, property, or zero-argument method. - Dict comparison:
is_equal_to(ignore=..., include=...)for selective key/field matching across dicts, dataclasses, namedtuples, Pydantic models, attrs, and plain objects - by name, regex, or type. - Recursive comparison:
is_equal_to()withtolerance,comparators, orignore_nullfor nested structures. - Extracting: flatten collections on attributes with
filterandsortsupport.
Testing
- Soft assertions: thread-safe, async-safe via
contextvars; each collected failure is reported with itsfile:line. Group errors withsa.group(), or useassert_all(). - Polling assertions:
eventually()(async) /eventually_sync()(blocking) retry for eventual consistency, with a convergence trace pinpointing why a timeout never settled. - Expected exceptions:
raises().when_called_with()then assert on the message, walk the cause chain (caused_by(),has_root_cause()), match anExceptionGroup(contains_error()), or pivot to the exception object (raised()). - Structured errors:
AssertionFailurewith.actual,.expected,.diffattributes. - Rich pytest diffs: recursive structural diffs across lists, sets, dicts, dataclasses, namedtuples, Pydantic models, and matchers, with circular-reference protection.
- Snapshot testing: store and compare data structures in JSON format; update via
--assertpy2-snapshot-update.matches_contract_snapshot()catches structural regressions, value-tolerant.
Extensibility
- Custom matchers:
register_matcher()for domain-specific matchers, composable with&,|,~. - Regex group extraction:
extracting_group()andmatches_with_groups()for regex captures. - Extensions:
add_extension()for custom assertion methods.
Optional adapters, each its own extra; full configuration and examples are in the Integrations guide.
- Allure (
pip install assertpy2[allure]): the pytest plugin auto-attaches structured diff and actual/expected data to Allure reports, in three configurable modes. - Behave (
pip install assertpy2[behave]): ready-made parameter types (PositiveInt,NonEmptyString, ...) for step definitions like{age:PositiveInt}. - JSON (
pip install assertpy2[json]): JSONPath navigation (at_json_path(),has_json_path()) and JSON Schema validation (matches_json_schema()). - Data frames (
pip install assertpy2[pandas]/[polars]/[numpy]): fluent equality for pandas/polars frames and numpy arrays, carrying each library's own diff.

