Skip to main content

assertpy2
A 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


Quick start

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).contains_entry({"name": "Alice"})
    assert_that(user["age"]).is_between(18, 120)
    assert_that(user["roles"]).contains("viewer").does_not_contain("admin")

The full documentation covers every assertion, matcher, and integration.

Failures that point at the difference
A recursive diff names the exact path that differs, in color, instead of dumping both structures.
Type-aware autocomplete
assert_that() returns a protocol per value type, so your IDE offers the methods that fit.
Typed narrowing
An assertion hands the value back statically narrowed, with no cast and no bare assert.
Composable matchers
45 matchers that combine with &, |, ~ and nest inside the expected structure itself.
Built for test suites
Soft assertions, polling for eventual consistency, snapshots, and expected-exception chains.
Integrations
Allure, Behave, JSON Path and Schema, pandas, polars, numpy, and OpenAPI response contracts.

Why fluent assertions?

assert states a condition well, and pytest reports it well.

What it cannot say is where two structures differ. It prints both and leaves the reading to you:

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 names the exact path, 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

It recurses through nested containers, and matcher predicates get the same treatment.

For dynamic fields like IDs, assert a subset with matches_structure().

The chain is the other half: one statement carries the whole intent, and your IDE offers only the methods that fit the value.

assert_that(items).is_instance_of(list).is_length(3).contains("admin")

Matchers are ordinary values that answer ==, the way unittest.mock.ANY does.

Nothing is patched, so a matcher can sit inside the expected structure itself, at any depth:

response = {"id": 7, "user": {"name": "Alice", "age": 30}, "tags": ["a", "b"]}

assert_that(response).is_equal_to(
    {
        "id": match.greater_than(0),
        "user": {"name": "Alice", "age": match.between(18, 120)},
        "tags": ["a", "b"],
    }
)

# or keep the bare `assert`, and pytest's own rewriting reports it
assert response == {
    "id": match.greater_than(0),
    "user": match.ignore(),
    "tags": ["a", "b"],
}

The fluent form keeps the path-level diff, the bare form keeps pytest's.

There are 45 matchers, combining with &, | and ~.

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

Type-aware autocomplete

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

11 type-specific Protocols instead of one Any.
Works in PyCharm, VS Code, and any LSP-compatible editor.

Typed narrowing

An assertion hands the value back, statically narrowed.

is_not_none() strips None, is_instance_of() narrows to the class, and .value returns it:

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 payload against a Pydantic model and narrows to it. exact=True catches contract drift:

data = assert_conforms(response.json(), OrderModel).value  # data: OrderModel

A failure you can read from code

An exception is the right default, and a dead end for anything that wants to read the result.

check() runs the next assertion for its verdict instead:

response = {"user": {"name": "Alice", "role": "superadmin"}, "status": "active"}
expected = {"user": {"name": "Alice", "role": "admin"}, "status": "active"}

outcome = assert_that(response).check().is_equal_to(expected)

if not outcome and outcome.diff:
    print(outcome.diff.entries[0].path)  # user.role

It is truthy when the assertion held. When it did not, it carries .message, .actual, .expected and a walkable .diff, and so does AssertionFailure.

So a reporter reads structure instead of parsing a string. That is how the Allure integration works, and it is open to anything else you build.

Features

Fluent API

  • Structural matching: matches_structure() for declarative dict/API-response validation.
  • Recursive field assertions: all_fields_satisfy() / has_no_none_fields() apply a predicate to every leaf of an object graph.
  • Vacuous-assertion guard: --assertpy2-vacuous warns when a universal assertion passes over an empty collection, having checked nothing.
  • Universal negation: .not_ inverts any assertion, no 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.

Type safety

  • Refinement predicates: satisfies() takes a TypeIs predicate, so a domain check narrows the chain too.
  • Contract testing: assert_conforms() validates a raw payload against a Pydantic model and narrows to it. exact=True catches contract drift, each=True validates list endpoints.

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()), search an ExceptionGroup (contains_error(), errors(), error_of()), or pivot to the object (raised()).
  • HTTP responses: assert on the response itself and every failure names the request it came from, with decoded_as_json() to step into the body. No client library is a dependency.
  • Structured errors: AssertionFailure carries .actual, .expected and .diff, and the diff renders into the message, so it shows off pytest too.
  • Assertions as values: check() runs the next assertion for its verdict instead of raising, handing back an AssertionOutcome.
  • Rich pytest diffs: recursive diffs across containers, dataclasses, attrs and Pydantic models, with intra-line carets for strings.
  • Snapshot testing: an external JSON file, an inline value recorded into the test source, or a value-tolerant contract, all updated with --assertpy2-snapshot-update.
  • OpenAPI response contracts: conforms_to_openapi() checks a JSON body against an operation's response schema, reporting every violation with its JSON path.

Extensibility

  • Custom matchers: register_matcher() composes existing ones, BaseMatcher carries its own predicate. Both compose with &, |, ~.
  • Custom assertions: add_extension() adds a method to the builder.
  • Regex group extraction: extracting_group() and matches_with_groups() for regex captures.

Integrations

  • 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

Download files

Download the file for your platform. If you're not sure which to choose, learn more about installing packages.

Source Distribution

assertpy2-2.21.0.tar.gz (737.0 kB view details)

Uploaded Source

Built Distribution

If you're not sure about the file name format, learn more about wheel file names.

assertpy2-2.21.0-py3-none-any.whl (244.5 kB view details)

Uploaded Python 3

File details

Details for the file assertpy2-2.21.0.tar.gz.

File metadata

  • Download URL: assertpy2-2.21.0.tar.gz
  • Upload date:
  • Size: 737.0 kB
  • Tags: Source
  • Uploaded using Trusted Publishing? Yes
  • Uploaded via: twine/7.0.0 CPython/3.13.14

File hashes

Hashes for assertpy2-2.21.0.tar.gz
Algorithm Hash digest
SHA256 3f9ccd6604f106c7285d1782bb634c063d1b1394d34065f8e0baf105297ad3d5
MD5 41c6acafc2634a10508a3df4e19ff71a
BLAKE2b-256 68f06e9e69292dd2e6891ddf9ce3c8ce62486d166453df78c6f91f052deb7929

See more details on using hashes here.

Provenance

The following attestation bundles were made for assertpy2-2.21.0.tar.gz:

Publisher: publish.yml on Solganis/assertpy2

Attestations: Values shown here reflect the state when the release was signed and may no longer be current.

File details

Details for the file assertpy2-2.21.0-py3-none-any.whl.

File metadata

  • Download URL: assertpy2-2.21.0-py3-none-any.whl
  • Upload date:
  • Size: 244.5 kB
  • Tags: Python 3
  • Uploaded using Trusted Publishing? Yes
  • Uploaded via: twine/7.0.0 CPython/3.13.14

File hashes

Hashes for assertpy2-2.21.0-py3-none-any.whl
Algorithm Hash digest
SHA256 0b689dcfe4e3d9d2ce8a131e2e9cbb9bcd9cca8d09d0969b045f63e3e3336dc3
MD5 ee2125dfb48f9b44e8efc5755b349c94
BLAKE2b-256 7d34d20442110436752dcdd652a877f5bda861ab299c4941ae14c3df6e7325af

See more details on using hashes here.

Provenance

The following attestation bundles were made for assertpy2-2.21.0-py3-none-any.whl:

Publisher: publish.yml on Solganis/assertpy2

Attestations: Values shown here reflect the state when the release was signed and may no longer be current.

Release history Release notifications | RSS feed

2.23.0

2 files

2.22.0

2 files

This release

2.21.0 This release

2 files

2.20.1

2 files

2.20.0

2 files

2.19.0

2 files

2.18.0

2 files

2.17.0

2 files

2.16.0

2 files

2.15.0

2 files

2.14.0

2 files

2.13.0

2 files

2.12.0

2 files

2.11.0

2 files

2.10.0

2 files

2.9.1

2 files

2.9.0

2 files

2.8.1

2 files

2.8.0

2 files

2.7.0

2 files

2.6.0

2 files

2.5.1

2 files

2.5.0

2 files

2.4.0

2 files

2.3.8

2 files

2.3.7

2 files

2.3.6

2 files

2.3.5

2 files

2.3.4

2 files

2.3.3

2 files

2.3.2

2 files

2.3.1

2 files

2.3.0

2 files

2.2.0

2 files

2.1.4

2 files

2.1.3

2 files

2.1.2

2 files

2.1.1

2 files

2.1.0

2 files

2.0.1

2 files

2.0.0

2 files

Supported by

AWS Cloud computing and Security Sponsor Datadog Monitoring Depot Continuous Integration Fastly CDN Google Download Analytics Sentry Error logging StatusPage Status page