Migration from assertpy¶
assertpy2 is a drop-in, fully typed replacement for the original
assertpy - the same assert_that(...) API, so for most projects
the switch is a single import.
TL;DR
On Python 3.10+, replace from assertpy import ... with from assertpy2 import ... and run your
tests. The assertions you already use carry over, apart from the few answers listed under
Where results differ.
Before you start¶
- Python 3.10+ is required. The original assertpy ran on Python 2.7 and 3.x. assertpy2 targets 3.10 through 3.15. If you are on an older interpreter, upgrade Python first. That is the only hard requirement of the migration.
- No runtime dependencies on Python 3.11+. On 3.10 a single tiny backport (
typing_extensions) is pulled in automatically. The extras ([json],[inline],[data]or one of[numpy],[pandas],[polars],[allure],[behave]) stay opt-in.
Switch the import¶
Swap the dependency first. Uninstall assertpy, install assertpy2, update your
requirements/pyproject, then switch the imports:
If you import the module instead of its names, alias it so existing call sites keep working:
A project-wide find-and-replace of from assertpy import to from assertpy2 import is usually the
entire migration.
Automate it across a project
grep -rl --include="*.py" "assertpy" . \
| xargs sed -i 's/from assertpy import/from assertpy2 import/g'
from assertpy import lines. For
import assertpy usages, prefer the alias shown above.
What stays the same¶
assertpy2 is a superset of the original, so the assertions you already use are present and, outside the cases under Where results differ, produce the same results:
- the
assert_that()entry point and fluent (return self) chaining - strings, numbers, lists, tuples, dicts, sets, booleans,
None, dates, files, and objects - dynamic
has_<name>()assertions andextracting()withfilterandsort - soft assertions, expected exceptions (
raises().when_called_with()),fail(),assert_warn(), snapshot testing, andadd_extension() - the existing failure messages, apart from the extra detail noted below.
Backward-compatible failures
Failing assertions now raise AssertionFailure, a subclass of AssertionError. Existing
except AssertionError handlers keep working unchanged. The subclass simply carries extra
structured data.
If your tests assert on message text
assertpy2 writes more into some failures than the original did, usually by adding to the end of
the message. A pytest.raises(match=...) or a substring check keeps passing.
What a substring check does not survive is a change of order: a mapping's keys are printed in
the order they were written rather than sorted, so a check pinned to the sorted rendering needs
updating. Comparing a whole message with == breaks on both counts, on the switch and between
assertpy2 versions.
Prefer matching a substring. Better still, skip the text and assert on the structured data the
failure carries: AssertionFailure.actual, .expected and .diff.
Where results differ¶
A few answers the original gave were wrong, and assertpy2 corrects them. A test can change its outcome on the switch in either direction, and each row below is measured against assertpy 1.1:
| Call | assertpy | assertpy2 |
|---|---|---|
is_close_to() with a bool value, expected value or tolerance |
measured True as 1 |
raises TypeError |
is_close_to() with a NaN tolerance |
passed | raises ValueError |
is_close_to() and is_not_close_to() on datetimes with a negative timedelta tolerance |
found nothing close, so is_not_close_to() passed for any two |
raises ValueError, as a negative number does |
a NaN as the value or the operand of is_less_than(), is_greater_than(), their _or_equal_to forms and is_close_to(), or as the value of is_between(), is_positive() and is_negative() |
passed | fails |
| an infinity against a finite number under an infinite tolerance | passed | fails |
is_in() and is_not_in() when the items hold the very NaN object under test |
is_in() failed and is_not_in() passed |
the reverse, as in answers |
is_equal_to() on a type whose __ne__ is not the negation of its __eq__ |
decided by != |
decided by == |
contains() and does_not_contain() on a class iterated only through __getitem__ |
searched it | raise TypeError |
add_extension() with a name already in use, by a built-in or by another function |
replaced it | raises ValueError unless override=True |
What improves automatically¶
You get these the moment you switch, without touching any test code:
- Thread-safe and async-safe soft assertions. State is isolated per thread and per
asyncio.Taskviacontextvars, so soft assertions are safe underpytest-xdistandasyncio. The original's soft assertions were not thread-safe. - Structured failures and rich diffs. Failures carry
.actual/.expected/.diff, and the diff is rendered into the message itself, so it shows inunittest, plain scripts and CI logs. Under pytest the auto-registered plugin attaches it to the failure as a section instead, which the terminal, JUnit reports and IDE runners all show, recursive for lists, dicts, dataclasses, namedtuples, attrs classes and Pydantic models. Setassertpy2_diff = "off"to turn that section off. - The comparison window of an IDE. PyCharm's runner gave the original's failures its "Click to see
difference" link by reading their message. A failed
is_equal_to()orhas_<name>()reaches the same runner through the hook pytest calls for a failedassert left == right, with the two values it compared. See The comparison window of an IDE. - Failures that say why. Where one fact explains a failure, a line under the message says it: an id the payload holds as text, a NaN, two instances compared by identity. See Some failures say why.
- Static typing. With
py.typedand@overloadprotocols your editor filters autocomplete by the value's type, and a type checker flags misuse before the tests run. See Type Safety.
What you can now adopt¶
New capabilities the original never had, ready whenever you want them:
- Composable matchers, reusable across assertions and the plain
==form. - Structural matching for API-response shapes.
- Typed narrowing (
.value) and contract testing for typed API responses. - Exception cause chains and groups.
- The collection pipeline and universal negation.
- Async and blocking polling for eventual consistency.
- JSON Path / Schema, regex group extraction, and bytes assertions.
See the comparison for how the two read side by side, and stability for what is covered by a compatibility promise and what is not.