Skip to content

Solganis/assertpy2

Repository files navigation

assertpy2

Fluent assertion library for Python with composable matchers, structural matching, and full type safety.
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_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)

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

Recursive diffs work for dicts, dataclasses, namedtuples, and Pydantic models. For responses with dynamic fields (IDs, timestamps), validate a subset with matches_structure() instead of exact equality.

The same path-level treatment for dicts, lists, sets, and matcher predicates:

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.

See the Type Safety guide for the full walkthrough.

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 to each mismatch 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().
  • Fluent chaining: write assertions as readable one-liners that chain naturally.

Built-in types

Testing

  • Soft assertions: thread-safe, async-safe via contextvars. Group errors with sa.group(), or use assert_all().
  • Async assertions: eventually() with polling/retry for eventual consistency.
  • Structured errors: AssertionFailure with .actual, .expected, .diff attributes.
  • 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

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.

See the full documentation for all assertion methods, examples, and advanced features.

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