Skip to content

Type Assertions

assert_that() returns a type-specific set of assertions. The sections below group them by value type.

Strings

assert_that("").is_empty().is_false().is_type_of(str)
assert_that("foo").is_length(3).is_not_empty().is_alpha().is_lower()
assert_that("123").is_digit()
assert_that("FOO").is_upper()
assert_that("foo").is_equal_to("foo").is_not_equal_to("bar")
assert_that("foo").is_equal_to_ignoring_case("FOO")
assert_that("foo bar").is_equal_to_ignoring_whitespace("foobar")
assert_that("foo").is_length_between(1, 5)
assert_that("foo123").is_alphanumeric()
assert_that("   ").is_whitespace()

assert_that("foo").contains("f", "oo")
assert_that("foo").contains_ignoring_case("F", "oO")
assert_that("foo").does_not_contain("x")
assert_that("foo").contains_only("f", "o")
assert_that("foo").contains_sequence("o", "o")
assert_that("foobar").contains_any_of("foo", "xyz")
assert_that("foobar").contains_none_of("xyz", "abc")
assert_that("foo").contains_duplicates()
assert_that("fox").does_not_contain_duplicates()

assert_that("foo").is_in("foo", "bar", "baz")
assert_that("foo").is_subset_of("abcdefghijklmnopqrstuvwxyz")
assert_that("foo").starts_with("f").ends_with("oo")
assert_that("FooBar").starts_with_ignoring_case("foo").ends_with_ignoring_case("BAR")

assert_that("foo").matches(r"\w")
assert_that("123-456-7890").matches(r"\d{3}-\d{3}-\d{4}")
assert_that("foo").does_not_match(r"\d+")

Regex matching

Use raw strings (r"...") for patterns. matches() passes on partial matches (like the underlying re.match). Anchor the pattern (^...$) to match the whole string. Inline flags such as (?m) and (?s) work, even though matches() takes no flags argument.

assert_that("foo").matches(r"\w{2}")     # partial, passes
assert_that("foo").matches(r"^\w{3}$")   # whole string, passes
assert_that("foo").matches(r"^\w{2}$")   # fails

Numbers

assert_that(0).is_zero().is_false().is_type_of(int)
assert_that(1).is_not_zero().is_positive()
assert_that(-1).is_negative()
assert_that(4).is_even()
assert_that(3).is_odd()
assert_that(9).is_divisible_by(3)

assert_that(123).is_equal_to(123).is_not_equal_to(456)
assert_that(123).is_greater_than(100).is_greater_than_or_equal_to(123)
assert_that(123).is_less_than(200).is_less_than_or_equal_to(200)
assert_that(123).is_between(100, 200)
assert_that(123).is_close_to(100, 25)
assert_that(1).is_in(0, 1, 2, 3).is_not_in(-1, -2, -3)

# floats
assert_that(123.4).is_close_to(123, 0.5)
assert_that(123.4).is_between(100.1, 200.2)
assert_that(float("NaN")).is_nan()
assert_that(123.4).is_not_nan()
assert_that(float("Inf")).is_inf()
assert_that(123.4).is_not_inf()

Floats and equality

Avoid is_equal_to() with float values. Use is_close_to() or is_between() instead.

Lists

assert_that([]).is_empty().is_type_of(list).is_iterable()
assert_that(["a", "b"]).is_length(2).is_not_empty()
assert_that(["a", "b"]).is_equal_to(["a", "b"]).is_not_equal_to(["b", "a"])

assert_that(["a", "b"]).contains("b", "a")
assert_that(["a", "b"]).does_not_contain("x", "y")
assert_that(["a", "b"]).contains_only("a", "b")
assert_that(["a", "b", "c"]).contains_sequence("b", "c")
assert_that(["a", "b", "c"]).contains_exactly("a", "b", "c")
assert_that(["c", "a", "b"]).contains_exactly_in_any_order("a", "b", "c")
assert_that(["a", "x", "b", "y", "c"]).contains_in_order("a", "b", "c")
assert_that(["a", "b"]).is_subset_of(["a", "b", "c"])
assert_that(["a", "b", "c"]).is_sorted()
assert_that(["c", "b", "a"]).is_sorted(reverse=True)
assert_that(["a", "x", "x"]).contains_duplicates()
assert_that(["a", "b", "c"]).does_not_contain_duplicates()
assert_that(["a", "b", "c"]).starts_with("a").ends_with("c")

assert_that([1, -2, 3]).any_satisfy(lambda x: x < 0)
assert_that([1, 2, 3]).all_satisfy(lambda x: x > 0)
assert_that([1, 2, 3]).none_satisfy(lambda x: x < 0)

assert_that([2, 4, 6]).satisfies_exactly(
    lambda x: x == 2, lambda x: x == 4, lambda x: x == 6
)
assert_that([4, 2]).satisfies_exactly_in_any_order(lambda x: x == 2, lambda x: x == 4)
assert_that([1, 2, 3]).zip_satisfies([2, 4, 6], lambda actual, other: other == actual * 2)
assert_that([1, 2, 3]).contains_only_once(1, 3)
assert_that([1, 2, 3]).has_same_size_as(("a", "b", "c"))
assert_that([1, 2, 3]).has_size_greater_than(2).has_size_less_than(4).has_size_between(
    1, 5
)

any_satisfy, all_satisfy, and none_satisfy accept both callables and matchers.

The exact-pairing and multiset assertions:

  • satisfies_exactly - pairs the i-th item with the i-th matcher (equal length required). satisfies_exactly_in_any_order - any one-to-one pairing instead.
  • zip_satisfies - checks a two-arg predicate over items zipped with another iterable.
  • contains_only_once - each given item must occur exactly once.
  • has_same_size_as - compares lengths against another sized object.
  • contains_exactly_in_any_order - multiset equality: exact items and counts, order ignored.

Lists of lists can be flattened by index with extracting (see dict flattening):

people = [["Fred", "Smith"], ["Bob", "Barr"]]
assert_that(people).extracting(0).is_equal_to(["Fred", "Bob"])
assert_that(people).extracting(-1).is_equal_to(["Smith", "Barr"])

Tuples

Tuples support the same membership, ordering, and duplicate assertions as lists:

assert_that(()).is_empty().is_type_of(tuple).is_iterable()
assert_that((1, 2, 3)).is_length(3).is_equal_to((1, 2, 3))
assert_that((1, 2, 3)).contains(3, 2, 1).contains_only(1, 2, 3)
assert_that((1, 2, 3)).contains_sequence(2, 3).contains_exactly(1, 2, 3)
assert_that((1, 5, 2, 8, 3)).contains_in_order(1, 2, 3)
assert_that((1, 2, 3)).is_subset_of((1, 2, 3, 4)).is_sorted()
assert_that((1, 2, 2)).contains_duplicates()
assert_that((1, 2, 3)).starts_with(1).ends_with(3)

Tuples of tuples flatten by index with extracting:

points = ((1, 2, 3), (4, 5, 6))
assert_that(points).extracting(0).is_equal_to([1, 4])
assert_that(points).extracting(-1).is_equal_to([3, 6])

Dicts

assert_that({}).is_empty().is_type_of(dict)
assert_that({"a": 1, "b": 2}).is_length(2).is_not_empty()
assert_that({"a": 1, "b": 2}).is_equal_to({"b": 2, "a": 1})

assert_that({"a": 1, "b": 2}).contains("b", "a")
assert_that({"a": 1, "b": 2}).does_not_contain("x", "y")
assert_that({"a": 1, "b": 2}).contains_only("a", "b")
assert_that({"a": 1, "b": 2}).is_subset_of({"a": 1, "b": 2, "c": 3})

# contains_key / does_not_contain_key are aliases of contains / does_not_contain
assert_that({"a": 1, "b": 2}).contains_key("b", "a")
assert_that({"a": 1, "b": 2}).does_not_contain_key("x", "y")

assert_that({"a": 1, "b": 2}).contains_value(2, 1)
assert_that({"a": 1, "b": 2}).does_not_contain_value(3, 4)

assert_that({"a": 1, "b": 2}).contains_entry({"a": 1}, {"b": 2})
assert_that({"a": 1, "b": 2}).does_not_contain_entry({"a": 2})

Selective comparison (ignore / include)

is_equal_to() can ignore or include specific keys or fields - across dicts, dataclasses, namedtuples, Pydantic models, attrs, and plain objects (for sequences, each element is compared pairwise with the same filters).

The filter accepts a single key, a nested-path tuple, or a list/set/frozenset of those. Any other iterable (a generator, an iterator, dict.keys()) raises TypeError.

# ignore keys (single, list/set/frozenset, or nested tuple)
assert_that({"a": 1, "b": 2}).is_equal_to({"a": 1}, ignore="b")
assert_that({"a": 1, "b": 2, "c": 3}).is_equal_to({"a": 1}, ignore={"b", "c"})
assert_that({"a": 1, "b": {"c": 2, "d": 3}}).is_equal_to(
    {"a": 1, "b": {"c": 2}}, ignore=("b", "d")
)

# include only specific keys
assert_that({"a": 1, "b": 2, "c": 3}).is_equal_to({"a": 1, "b": 2}, include=["a", "b"])

# objects with introspectable fields
@dataclass
class User:
    id: int
    name: str
    email: str

assert_that(User(1, "Alice", "a@x.com")).is_equal_to(
    User(99, "Alice", "a@x.com"), ignore="id"
)

# attrs instances work the same, including nested paths
@attrs.define
class Account:
    id: int
    owner: str

assert_that(Account(1, "Alice")).is_equal_to(Account(99, "Alice"), ignore="id")

ignore and include also accept a re.Pattern (matched against field names) or a type (matched against field values):

import re

# ignore private-ish keys (matched against field names)
assert_that(payload).is_equal_to(expected, ignore=re.compile(r"^_"))
# ignore all float fields
assert_that(payload).is_equal_to(expected, ignore=float)

Recursive comparison (tolerance / custom comparators)

is_equal_to() can compare two concrete nested structures with a numeric tolerance or with custom comparators, anywhere in the graph:

  • tolerance - an absolute tolerance applied to every real-number leaf (abs(actual - expected) <= tolerance).
  • comparators - maps a type or a field name to an (actual, expected) -> bool predicate (a field-name key wins over a type key).

Tolerated or comparator-equal leaves are reported in neither the message nor the diff.

# absolute float tolerance, at any depth
assert_that({"point": {"x": 1.0001, "y": 2.0}}).is_equal_to(
    {"point": {"x": 1.0, "y": 2.0}}, tolerance=0.001
)

# comparator by type, or by field name
assert_that(order).is_equal_to(
    expected, comparators={float: lambda a, e: round(a, 2) == round(e, 2)}
)
# case-insensitive comparator by field name
assert_that(order).is_equal_to(
    expected, comparators={"name": lambda a, e: a.lower() == e.lower()}
)

Use comparators to change how a field or type is compared. To drop a field from the comparison entirely, use ignore (above) rather than an always-true comparator (ignore also handles fields missing on one side or with incomparable types).

ignore_null=True skips any named field the expected side leaves None, at any depth - handy for a partial expected/template. Only the expected side is skipped, so an unexpectedly None actual field is still reported (never masked):

# compare only the fields the expected template sets;
# age and address, left None, are ignored
assert_that(user).is_equal_to(User(name="Alice"), ignore_null=True)

Sequence elements have no field name, so a comparators field-name key does not apply to them (use a type key or tolerance). Sets compare by standard equality.

Dict flattening

Lists of dicts can be flattened on a key with extracting (see extracting attributes):

people = [{"first_name": "Fred"}, {"first_name": "Bob"}]
assert_that(people).extracting("first_name").is_equal_to(["Fred", "Bob"])

Dict key assertions

Assert against the value of a key by prepending has_ to the key name (see dynamic assertions):

fred = {"first_name": "Fred", "last_name": "Smith", "shoe_size": 12}
assert_that(fred).has_first_name("Fred").has_shoe_size(12)

Sets

assert_that(set()).is_empty().is_type_of(set)
assert_that({"a", "b"}).is_length(2).is_equal_to({"b", "a"})
assert_that({"a", "b"}).contains("b", "a").does_not_contain("x")
assert_that({"a", "b"}).contains_only("a", "b")
assert_that({"a", "b"}).is_subset_of({"a", "b", "c"})
assert_that({"a", "b"}).is_subset_of({"a"}, {"b"})

Booleans

assert_that(True).is_true()
assert_that(False).is_false()
assert_that(True).is_type_of(bool)

None

assert_that(None).is_none()
assert_that("").is_not_none()
assert_that(None).is_type_of(type(None))

Dates

assertpy2 supports dates via the datetime type.

import datetime

today = datetime.datetime.today()
yesterday = today - datetime.timedelta(days=1)

assert_that(yesterday).is_before(today)
assert_that(today).is_after(yesterday)
assert_that(today).is_before_or_equal_to(today)
assert_that(today).is_after_or_equal_to(yesterday)

Equality can ignore units of time, and the numeric comparisons work on dates too:

assert_that(today).is_equal_to_ignoring_milliseconds(today_0us)
assert_that(today).is_equal_to_ignoring_seconds(today_0s)
assert_that(today).is_equal_to_ignoring_time(today_0h)

assert_that(middle).is_between(yesterday, today)
# tolerance is a timedelta
assert_that(yesterday).is_close_to(today, datetime.timedelta(hours=24))

Date properties can be asserted dynamically with has_<property> (see dynamic assertions):

x = datetime.datetime(1980, 1, 2, 3, 4, 5, 6)
assert_that(x).has_year(1980).has_month(1).has_day(2)
assert_that(x).has_hour(3).has_minute(4).has_second(5).has_microsecond(6)

Files

assert_that("foo.txt").exists().is_file()
assert_that("missing.txt").does_not_exist()
assert_that("mydir").is_directory()

assert_that("foo.txt").is_named("foo.txt").is_child_of("mydir")
assert_that("foo.txt").is_readable().is_writable()
assert_that("/usr/bin/python").is_executable()

Read a file into a string with contents_of() (default encoding utf-8) and continue with string assertions:

from assertpy2 import assert_that, contents_of

assert_that(contents_of("foo.txt", "ascii")).starts_with("foo").ends_with(
    "bar"
).contains("oob")

Bytes / bytearray

Assertions for bytes and bytearray values:

assert_that(b"hello world").is_valid_utf8()
assert_that(b"hello").is_valid_encoding("ascii")
assert_that(b"\x89PNG\r\n\x1a\n...").starts_with_bytes(b"\x89PNG")
assert_that(b"hello world").contains_bytes(b"world")
assert_that(b"\x89PNG").has_byte_at(0, 0x89)            # IndexError if out of range
assert_that(b"\xab\xcd\xef").is_hex_equal_to("abcdef")

decoded_as() returns a new builder with the decoded string so string assertions can continue (UnicodeDecodeError is raised if decoding fails):

assert_that(b"hello").decoded_as("utf-8").starts_with("hel").is_length(5)
assert_that(b"hello").decoded_as().is_equal_to("hello")  # default encoding utf-8

All bytes assertions work with soft assertions, warn mode, and .not_ negation.

Objects

fred = Person("Fred", "Smith")

assert_that(fred).is_not_none().is_type_of(Person).is_instance_of(object)
assert_that(fred).is_instance_of_any(Person, dict)
assert_that(Person).is_subclass_of(object)
assert_that(fred).is_same_as(fred)
assert_that(fred.say_hello).is_callable()
assert_that(fred.first_name).is_not_callable()

assert_that(fred.first_name).is_equal_to("Fred")
assert_that(fred.name).is_equal_to("Fred Smith")          # property
assert_that(fred.say_hello()).is_equal_to("Hello, Fred!")  # method

Recursive field assertions

all_fields_satisfy walks the whole object graph (mappings, dataclasses, namedtuples, attrs classes, Pydantic models, lists, tuples) and applies one matcher or callable to every scalar leaf, reporting the path of each leaf that does not satisfy it. has_no_none_fields is the common special case:

assert_that({"a": 1, "nested": {"b": 2}}).all_fields_satisfy(match.is_positive())
assert_that([1, [2, 3]]).all_fields_satisfy(lambda x: x > 0)
assert_that({"id": 1, "profile": {"name": "Alice"}}).has_no_none_fields()

assert_that({"a": 1, "b": {"c": -2}}).all_fields_satisfy(match.is_positive())  # fails
# Expected all fields to satisfy a positive value, but 1 field did not.
#   b.c: expected a positive value, but was -2

Scalars, strings and sets are treated as single leaves (use each / all_satisfy for element-wise set checks), and circular references are reported once rather than recursed into.

Extracting attributes from objects

Flatten a collection of objects on an attribute, property, or zero-argument method with extracting:

people = [Person("Fred", "Smith"), Person("Bob", "Barr")]

assert_that(people).extracting("first_name").contains("Fred", "Bob")
assert_that(people).extracting("first_name", "last_name").contains(
    ("Fred", "Smith"), ("Bob", "Barr")
)
# property
assert_that(people).extracting("name").contains("Fred Smith", "Bob Barr")
# zero-argument method
assert_that(people).extracting("say_hello").contains("Hello, Fred!", "Hello, Bob!")

It also works on collections of dicts (extracting by key), Pydantic models, and across subclasses in a mixed collection.

Filtering

filter keeps only items for which it is truthy. It may be a key/attribute name, a dict of key-value pairs that must all match, or a predicate:

users = [
    {"user": "Fred", "active": True, "age": 25},
    {"user": "Johnny", "active": True, "age": 18},
    {"user": "Bob", "active": False, "age": 30},
]

assert_that(users).extracting("user", filter="active").is_equal_to(["Fred", "Johnny"])
assert_that(users).extracting("user", filter={"active": False}).is_equal_to(["Bob"])
assert_that(users).extracting("user", filter=lambda x: x["age"] > 20).is_equal_to(
    ["Fred", "Bob"]
)

Sorting

sort orders the extracted items. It may be a key/attribute name, an iterable of names (tie-breaking left to right), or a key function:

assert_that(users).extracting("user", sort="age").is_equal_to(["Johnny", "Fred", "Bob"])
assert_that(users).extracting("user", sort=["active", "age"]).is_equal_to(
    ["Bob", "Johnny", "Fred"]
)
assert_that(users).extracting("user", sort=lambda x: -x["age"]).is_equal_to(
    ["Bob", "Fred", "Johnny"]
)

Dynamic assertions on objects

assertpy2 exposes has_<name>() for any attribute, property, or zero-argument method on the value, so attribute checks stay compact:

fred = Person("Fred", "Smith")

assert_that(fred).has_first_name("Fred")     # attribute
assert_that(fred).has_name("Fred Smith")     # property
assert_that(fred).has_say_hello("Hello, Fred!")  # zero-arg method

Dynamic assertions also work on dicts, keyed by entry name:

assert_that(
    {"first_name": "Fred", "last_name": "Smith"}
).has_first_name("Fred").has_last_name("Smith")

Exceptions

Exception and warning assertions wrap a callable rather than a value: you assert on what calling the function does, then chain assertions on the resulting message.

assert_that(some_func).raises(RuntimeError).when_called_with("foo")
assert_that(deprecated_func).warns(DeprecationWarning).when_called_with("foo")

See Errors & Reporting for the full set:

  • expected exceptions and warnings
  • the cause chain (caused_by(), has_root_cause()) and ExceptionGroup matching (contains_error())
  • pivoting to the raised exception (raised()) or the call's return value (returned()).