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
assertrewrites plainassertstatements 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 release1.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 writeassert 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:
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:
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:
@overloadprotocols andpy.typedgive 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.Taskviacontextvars). 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.