Skip to content

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:

from assertpy import assert_that, soft_assertions, fail

from assertpy2 import assert_that, soft_assertions, fail

If you import the module instead of its names, alias it so existing call sites keep working:

import assertpy2 as assertpy  # `assertpy.assert_that(...)` call sites stay unchanged

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'
Review the diff afterwards: the replacement only rewrites 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 and extracting() with filter and sort
  • soft assertions, expected exceptions (raises().when_called_with()), fail(), assert_warn(), snapshot testing, and add_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.Task via contextvars, so soft assertions are safe under pytest-xdist and asyncio. 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 in unittest, 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. Set assertpy2_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() or has_<name>() reaches the same runner through the hook pytest calls for a failed assert 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.typed and @overload protocols 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:

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.