Skip to main content

assertpy2
Fully typed fluent assertion library for Python
A modern, batteries-included fork of assertpy

CI Coverage PyPI version Python Downloads
every public overload checked by ty, mypy --strict, pyright and pyrefly 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. 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. Matcher predicates get the same treatment.

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

The chain is the other half. 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 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 the whole surface:

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

15 type-specific Protocols instead of one Any.
Works in PyCharm, VS Code, and any editor that runs a type checker.

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, pyright and pyrefly

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. Reading the result means catching it.

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. So does AssertionFailure.

So a reporter reads structure instead of parsing a string. That is how the Allure integration works. Anything else you build can too.

Features

Fluent API

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

Failure reporting

  • 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.
  • Failures that say why: where one fact explains a failure, a line under the message says it. An id the payload holds as text (7 against "7"), a NaN, two instances compared by identity, surrounding whitespace. A failed contains() or is_in() names the closest row, and forty rows that differ in one field are named as one place.

Plugin for pytest

Set from the command line or from [tool.pytest.ini_options], not from a call.

  • Failure clustering: forty failing tests are usually not forty problems. Where three or more differ at the same place, the run ends with a line saying where. On by default, assertpy2_failure_clusters = "off" turns it off.
  • IDE comparison window: a failed is_equal_to() or has_<name>() gets PyCharm's "Click to see difference" link, as a plain assert does, and the window shows the two values it compared. Under ignore= or include= those go without the keys left out. The plugin hands them to pytest_assertrepr_compare, the hook the IDE's runner listens to. On by default, assertpy2_comparison = "off" turns it off.
  • Diagnostic profiles: assertpy2_profile turns the guards below on in one line. compatible (default) leaves them off, safe warns, strict fails the tests they find. A setting you name yourself still wins.
  • Vacuous-assertion guard: --assertpy2-vacuous warns when a universal assertion passes over an empty collection, having checked nothing.
  • Dangling-assertion detector: --assertpy2-dangling warns when a chain builds an assertion and never runs it. assert assert_that(x).is_positive passes on any value, and neither ruff nor coverage sees it.

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.

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

Metadata

Release files for assertpy2 2.30.0

For a detailed explanation of source distributions (sdists) and built distributions (wheels), please see the package formats documentation.

Source distribution (sdist)

Source distribution for assertpy2 2.30.0
File Size Uploaded
assertpy2-2.30.0.tar.gz 1.5 MB Details

Built distribution (wheel)

Table of built distributions (wheels) for assertpy2 2.30.0
File Interpreter ABI Platform
assertpy2-2.30.0-py3-none-any.whl Python 3 none any Details

Total release size: 2.0 MB

Release files / assertpy2-2.30.0.tar.gz

Download URL assertpy2-2.30.0.tar.gz
Size 1.5 MB
Tags Source
SHA-256 checksum
How to use checksums
1b9ba5c1d79f1e3925f953b53abebec8c1b5392936cdd3317fe73114504936d6
BLAKE2b-256 checksum
How to use checksums
23e923538c86d79d2a08176c79d7b68a4f01787eeb7a9f0c93a1fc8c581dfa71
Upload date
Uploaded using Trusted Publishing?
What is trusted publishing?
Yes
Uploaded via twine/7.0.0 CPython/3.13.14

Provenance

Provenance describes where a file came from. On PyPI, provenance is shared via attestations, which provide a verifiable record of the build or publishing details. View details, limitations and caveats.

PyPI Publish Attestation

PyPI verified that this artifact, at this checksum, originated from the publisher listed below.

Signed by GitHub Actions, verified by PyPI on Oct 6, 2026.

Transparency log

Release files / assertpy2-2.30.0-py3-none-any.whl

Download URL assertpy2-2.30.0-py3-none-any.whl
Size 438.2 kB
Tags Python 3
SHA-256 checksum
How to use checksums
a69cc58bb4f376c910f4f51639c58b8369d34e16de6fdac48c92c15bec1ea37d
BLAKE2b-256 checksum
How to use checksums
1e51f2db5ebc1d856d4229711342b1c1bcf99bd0fc445eeadb296c82e9ef40d1
Upload date
Uploaded using Trusted Publishing?
What is trusted publishing?
Yes
Uploaded via twine/7.0.0 CPython/3.13.14

Provenance

Provenance describes where a file came from. On PyPI, provenance is shared via attestations, which provide a verifiable record of the build or publishing details. View details, limitations and caveats.

PyPI Publish Attestation

PyPI verified that this artifact, at this checksum, originated from the publisher listed below.

Signed by GitHub Actions, verified by PyPI on Oct 6, 2026.

Transparency log

Release history Release notifications | RSS feed

This release

2.30.0 This release

2 release files

2.29.0

2 release files

2.28.0

2 release files

2.27.1

2 release files

2.27.0

2 release files

2.26.0

2 release files

2.24.0

2 release files

2.23.0

2 release files

2.22.0

2 release files

2.21.0

2 release files

2.20.1

2 release files

2.20.0

2 release files

2.18.0

2 release files

2.17.0

2 release files

2.16.0

2 release files

2.12.0

2 release files

2.11.0

2 release files

2.10.0

2 release files

2.9.1

2 release files

2.9.0

2 release files

2.8.1

2 release files

2.8.0

2 release files

2.7.0

2 release files

2.6.0

2 release files

2.5.1

2 release files

2.5.0

2 release files

2.4.0

2 release files

2.3.8

2 release files

2.3.7

2 release files

2.3.6

2 release files

2.3.5

2 release files

2.3.4

2 release files

2.3.3

2 release files

2.3.2

2 release files

2.3.1

2 release files

2.3.0

2 release files

2.2.0

2 release files

2.1.4

2 release files

2.1.3

2 release files

2.1.2

2 release files

2.1.1

2 release files

2.1.0

2 release files

2.0.1

2 release files

2.0.0

2 release files

Anthropic, PBC Visionary sponsor Bloomberg Visionary sponsor Hudson River Trading Visionary sponsor Meta Visionary sponsor NVIDIA Visionary sponsor Microsoft Sustainability sponsor Depot Continuous Integration AWS Cloud computing and Security Sponsor Datadog Monitoring Fastly CDN Google Download Analytics Sentry Error logging StatusPage Status page