Skip to content

Comparison

assertpy2 is the only library compared here that gives you the fluent, matcher, and == styles in a single import, then goes further with static typing, thread- and async-safe soft assertions, async polling, and structured failures. The tables below are a side-by-side comparison with the common alternatives.

In short

assertpy2 unifies the fluent, matcher, and == styles in one typed package, then adds thread- and async-safe soft assertions, async polling, structured failures, and rich pytest diffs. It ships 36 composable matchers and over 100 assertion methods across 12 value types, with no runtime dependencies on Python 3.11+. One import, all three styles.

The approaches

  • pytest assert rewrites plain assert statements to produce detailed introspection on failure. There is no API to learn and the failure output is excellent, but there are no reusable conditions and no fluent chaining.
  • PyHamcrest is a matcher framework: assert_that(value, is_(greater_than(5))). assertpy2 provides the same composable-matcher model (&, |, ~, custom matchers) inside a typed fluent API.
  • assertpy (the original) introduced the fluent assert_that(x).is_...() chaining this project is built on. It has been inactive since 2020 (last release 1.1) and has no static typing. assertpy2 is its successor and substantially expands the assertion set - adding the entire bytes family, more string and collection assertions, structural matching, the collection pipeline, JSON Path/Schema, regex group extraction, async polling, universal negation, and composable matchers on top of the original. See Migrating from assertpy to switch.
  • dirty-equals (mis)uses __eq__ so you can write assert response == {"id": IsPositive(), ...}. assertpy2 matchers work the same way inside ==, so the same single dependency covers this style too.

In code

The same check - id is a positive integer and name is a non-empty string - in each library:

from assertpy2 import assert_that, match

assert_that(response).matches_structure({
    "id": match.is_positive(),
    "name": match.is_non_empty_string(),
})

# or the dirty-equals style, no wrapper:
assert response == {"id": match.is_positive(), "name": match.is_non_empty_string()}
assert response["id"] > 0
assert isinstance(response["name"], str) and response["name"]
from hamcrest import assert_that, has_entries, greater_than, instance_of, all_of, not_, empty

assert_that(response, has_entries({
    "id": greater_than(0),
    "name": all_of(instance_of(str), not_(empty())),
}))
from assertpy import assert_that

assert_that(response["id"]).is_positive()
assert_that(response["name"]).is_not_empty()
from dirty_equals import IsPositiveInt, IsStr

assert response == {"id": IsPositiveInt, "name": IsStr(min_length=1)}

Only assertpy2 offers both the typed structural form and the == form from a single import.

When it fails

A nested response, after role comes back as "superadmin". What each library prints on failure:

assert_that(response).matches_structure({...})
--- Structured Diff ---
diff (match):
  user.role: expected a value in <('admin', 'user')>, but was 'superadmin'
assert response == expected
E   Differing items:
E   {'user': {'name': 'Alice', 'role': 'superadmin', 'age': 30}} !=
E   {'user': {'name': 'Alice', 'role': 'admin', 'age': 30}}
assert response == {"user": {"role": IsOneOf("admin", "user"), ...}}
E   Differing items:
E   {'user': {'name': 'Alice', 'role': 'superadmin', 'age': 30}} !=
E   {'user': {'name': IsStr, 'role': IsOneOf('admin', 'user'), 'age': IsInt}}

Only assertpy2 prints the path (user.role) and the exact predicate that failed. dirty-equals and the assertpy2 == form both hand rendering to pytest, which dumps the whole differing container for you to scan. The fluent form trades the zero-import convenience of == for a path-level diff.

Style and typing

pytest assert PyHamcrest assertpy dirty-equals assertpy2
Paradigm rewritten assert matchers fluent chain == objects fluent + matchers + ==
Mix styles in one suite No No No No Yes
Static typing (py.typed, overloads) n/a No No Typed Yes
Autocomplete filtered by value type No No No No Yes
Fluent chaining No No Yes No Yes
Composable matchers No Yes No Yes Yes
Works inside plain == n/a No No Yes Yes

Assertions and matchers

pytest assert PyHamcrest assertpy dirty-equals assertpy2
Structural matching (nested) manual partial No Yes Yes
Collection / ordering assertions manual Yes Yes Yes Yes
Negation of any assertion (.not_) manual partial No partial Yes
Collection pipeline (map / filter / flatten / navigate) manual No No No Yes
Dynamic attribute assertions (has_<name>()) No No Yes No Yes
Regex group extraction manual No No No Yes
JSON Path / JSON Schema No No No IsJson only Yes
File / date / bytes assertions No No file, date date Yes (all)
Custom assertions or matchers functions Yes Yes Yes Yes (both)

Reporting, safety and tooling

pytest assert PyHamcrest assertpy dirty-equals assertpy2
Soft assertions plugin No Yes No Yes
Soft assertions thread-safe and async-safe n/a n/a No n/a Yes
Grouped soft assertions (sa.group) No No No No Yes
Async / eventual polling (eventually()) No No No No Yes
Structured failure data (.actual / .expected / .diff) No No No No Yes
Rich, recursive pytest diffs built-in No No No Yes
Snapshot testing plugin No Yes No Yes
Warn mode (non-failing assertions) No No Yes No Yes
Allure / Behave integrations No No No No Yes

Project health

pytest assert PyHamcrest assertpy dirty-equals assertpy2
Latest release built-in 2.1.0 1.1 (2020) 0.9.0 2.8.1
Property-based tests n/a No No No Yes
Runtime dependencies none none none none none on 3.11+
License MIT BSD BSD MIT BSD-3

Note

The property-based-tests row reflects each project's published test dependencies as of June 2026. assertpy2 ships a Hypothesis suite covering its comparison, diff, and matcher logic, on top of 100% branch coverage.

All three styles, one import

The styles above are not mutually exclusive in assertpy2. A single import and no runtime dependencies on Python 3.11+ give you all of them at once, and you can mix them freely in the same test suite:

from assertpy2 import assert_that, match

# fluent chaining (the assertpy heritage)
assert_that(value).is_positive().is_less_than(100)

# matchers inside plain == (the dirty-equals style)
assert response == {"id": match.is_positive(), "name": match.is_non_empty_string()}

# composable matchers (the Hamcrest style)
assert_that(value).satisfies(match.greater_than(0) & match.less_than(100))

What only assertpy2 does here

Across the columns above, assertpy2 is the only option that:

  • covers the fluent, matcher, and == styles in a single import, mixable in one suite, so there is no juggling of libraries;
  • is statically typed: @overload protocols and py.typed give autocomplete filtered by the value's type and usage verified by a type checker before the test runs;
  • has soft assertions that are both thread-safe and async-safe (independent state per thread and per asyncio.Task via contextvars). The original assertpy's soft assertions are not thread-safe, and the other tools have no soft assertions at all;
  • polls for eventual consistency with eventually(), for async operations and reactive systems;
  • attaches structured failure data (.actual / .expected / .diff) and renders rich, recursive diffs in pytest reports;
  • adds a collection pipeline, regex group extraction, dynamic has_<name>() assertions, snapshot testing, JSON Path and Schema validation, file/date/bytes assertions, and Allure/Behave integrations.

All of that with no runtime dependencies on Python 3.11+ (one tiny backport on 3.10): breadth that would otherwise require several separate libraries.