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.
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 atypeor a field name to an(actual, expected) -> boolpredicate (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¶
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()) andExceptionGroupmatching (contains_error()) - pivoting to the raised exception (
raised()) or the call's return value (returned()).