Fluent assertion library for Python with composable matchers, structural matching, and full type safety.
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_type_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 work for dicts, dataclasses, namedtuples, attrs, and Pydantic models.
For responses with dynamic fields (IDs, timestamps), validate a subset with
matches_structure() instead of exact equality.
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.
See the Type Safety guide for the full walkthrough.
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 to each mismatch on failure. - Universal negation:
.not_inverts any assertion without dedicatedis_not_*methods. - Collection pipeline:
filtered_on(),mapped(),flat_mapped(),first(),last(),element(),single(). - Fluent chaining: write assertions as readable one-liners that chain naturally.
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. - JSON assertions: JSONPath navigation and JSON Schema validation.
pip install assertpy2[json]. - Dynamic assertions:
has_<name>()for any attribute, property, or zero-argument method. - Dict comparison:
is_equal_to()withignoreandincludefor selective key/field matching (dicts, dataclasses, namedtuples, Pydantic models, attrs, plain objects). - Extracting: flatten collections on attributes with
filterandsortsupport.
Testing
- Soft assertions: thread-safe, async-safe via
contextvars. Group errors withsa.group(), or useassert_all(). - Async assertions:
eventually()with polling/retry for eventual consistency. - Structured errors:
AssertionFailurewith.actual,.expected,.diffattributes. - Rich pytest diffs: recursive structural diffs for lists, sets, strings, dicts, dataclasses, namedtuples, Pydantic models, and matcher-based assertions (
matches_structure(),satisfies(),each()). Circular reference protection. - Snapshot testing: store and compare data structures in JSON format.
- Property-based tested: comparison, selective-diff, matcher algebra, and collection logic are checked with Hypothesis against reference semantics, on top of 100% branch coverage.
Type safety
- Type-aware autocomplete: 9 Protocols, IDE shows only relevant methods per type.
- py.typed:
Selfreturn types, PEP 561 compliant (PEP 561).
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.
Integrations
- Allure: auto-attach structured diff and actual/expected data to reports.
pip install assertpy2[allure]. - Behave: ready-made parameter types for step definitions.
pip install assertpy2[behave].
See the full documentation for all assertion methods, examples, and advanced features.
When allure-pytest is installed, the pytest plugin auto-attaches structured failure data to Allure reports as JSON attachments.
pip install assertpy2[allure]Three modes controlled via pytest.ini (or pyproject.toml):
| Mode | What is attached |
|---|---|
diff (default) |
Structured Diff JSON (path-level breakdown) |
full |
Structured Diff + actual/expected JSON |
off |
Nothing |
# pyproject.toml
[tool.pytest.ini_options]
assertpy2_allure = "full"Ready-made parameter types for Behave step definitions:
pip install assertpy2[behave]# in environment.py or steps/conftest.py
from assertpy2.behave_matchers import register_assertpy_types
register_assertpy_types()Then use in step definitions:
@given('a user aged {age:PositiveInt}')
def step_impl(context, age):
context.age = age # already validated as int > 0Available types: PositiveInt, NonNegativeInt, PositiveFloat, NonEmptyString, BoolLike.
See the Integrations guide for attachment modes, configuration, and full examples.
