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 Coverage PyPI version Python Downloads
public overloads type-checked by ty, mypy --strict, and pyright with zero suppressions Documentation OpenSSF Scorecard


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")

The full documentation covers 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 on failure. Plain assert dumps both structures and leaves you to find the two wrong fields:

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

The diff recurses through nested containers, and matcher predicates get the same path-level treatment. For dynamic fields like IDs or timestamps, assert 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 hands the value back, statically narrowed. is_not_none() strips None, is_instance_of() narrows to the class, and .value returns it 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

Features

Fluent API

Type safety

Built-in types

Testing

  • Soft assertions: thread-safe and async-safe via contextvars, each failure reported with its file:line. Group with sa.group() or assert_all().
  • Polling assertions: eventually() (async) / eventually_sync() (blocking) retry for eventual consistency, with a convergence trace on timeout.
  • Expected exceptions: raises().when_called_with(), walk the cause chain (caused_by(), has_root_cause()), match ExceptionGroup (contains_error()), or pivot to the object (raised()).
  • Structured errors: AssertionFailure carries .actual, .expected, .diff, and the diff renders into the message itself, so it shows off pytest too.
  • Rich pytest diffs: recursive structural diffs across containers, dataclasses, attrs, and Pydantic models, with intra-line carets for strings and circular-reference protection.
  • Snapshot testing: three modes under one typed API, all updated with --assertpy2-snapshot-update: snapshot() (external JSON file), matches_inline() (recorded into the test source), and matches_contract_snapshot() (value-tolerant structural regressions).
  • OpenAPI response contracts: conforms_to_openapi(spec, path, method) validates a JSON response body against an operation's response schema (OpenAPI 3.0/3.1), reporting every violation with its JSON path.

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.
  • 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

4 stars

Watchers

0 watching

Forks

Packages

 
 
 

Contributors

Languages