Skip to content

Solganis/assertpy2

Repository files navigation

assertpy2

The fully-typed fluent assertion library for Python
A modern, batteries-included fork of assertpy

CI PyPI version Downloads Python Coverage
Documentation Ruff uv ty OpenSSF Scorecard OpenSSF Best Practices
public overloads type-checked by ty, mypy --strict, and pyright with zero suppressions


pip install assertpy2  # drop-in replacement for assertpy, just change the import
from 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)

Structured diff in the terminal: user.role shown with its path, removal in red and addition in green

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().

Structured diffs in the terminal: dict path, list element, set extra/missing, and structural-matcher predicate diffs, side by side

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 pyright

For 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: OrderModel

Returning the value it verified, statically narrowed, lets you assert and use the result in a single step.

Features

Fluent API

  • Composable matchers: match.greater_than(5), match.is_uuid(), combine with &, |, ~. Also work with plain assert ==.
  • 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 dedicated is_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_order variants.
  • 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: .value hands the checked value back; is_not_none(), is_instance_of(), and a satisfies() TypeIs predicate 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=True catches silent contract drift (undeclared fields), each=True validates list endpoints.
  • py.typed: Self return types, PEP 561 compliant (PEP 561).

Built-in types

Testing

  • Soft assertions: thread-safe, async-safe via contextvars; each collected failure is reported with its file:line. Group errors with sa.group(), or use assert_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 an ExceptionGroup (contains_error()), or pivot to the exception object (raised()).
  • Structured errors: AssertionFailure with .actual, .expected, .diff attributes.
  • 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() and matches_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.

BSD 3-Clause License

About

Fluent, fully-typed assertion library for Python: type-narrowing assertions, contract testing, composable matchers, and structural matching.

Topics

Resources

License

Contributing

Security policy

Stars

3 stars

Watchers

0 watching

Forks

Packages

 
 
 

Contributors

Languages