v0.2.0

lovely-assertions documentation

Fluent, strictly-typed assertions for Python tests: your editor offers only the assertions that fit the value, and a failure explains itself in a sentence.

4 min readEdit this page
python
from lovely_assertions import expect

hostname = "db-01.internal"
open_ports = [5432, 6379]
labels = {"tier": "primary", "region": "eu-west-1"}

expect(hostname).ends_with(".internal")
expect(open_ports).contains(5432).and_.has_length(2)
expect(labels).contains_entry("tier", "primary", because="a replica cannot take writes")

Three things make this different from writing assert by hand, and everything here follows from them:

  • Your editor only offers what applies. expect(name). on a str proposes starts_with; it does not propose is_positive. The subject you get is chosen from the value's type, statically and at runtime, by one table.
  • Narrowing is real. expect(raw).is_not_none().subject is a str to both pyright and mypy — not an object, not a cast.
  • A failure leads with a sentence. It names the value, what was expected, and what was there instead; a diff follows only where one says something the sentence cannot. Why that matters.

Start here

New to the library? Read these four, in order. About twenty minutes.

# Page What you get
1 Installation Installed, imported, working
2 Your first assertions The shape of every assertion you will write
3 Reading a failure How to get a message that ends the investigation
4 Chaining and narrowing and_, .subject, and how types flow through a chain

Coming from plain assert, assertpy, or unittest? Migrating maps what you write today onto what you would write here, and is honest about where a plain assert is still the better call.


Guides

Task-oriented. Each page answers "how do I assert this?", and each of the subject pages below links into the reference for the exhaustive list.

By what you are asserting on

Page Covers
Any value Equality, identity, None, truthiness, types, predicates — available on every subject
Strings Containment, prefixes, regex, wildcards, character classes
Numbers and booleans Comparisons, ranges, sign, tolerance, nan/inf, implies
Collections Membership, length, set relations, duplicates, per-item checks
Sequences Order, position, sorting, element-by-element comparison
Mappings Keys, values, entries — and why the difference shows up in the message
Dates and times Comparison, tolerance, time zones, durations
Paths Pure path shape, and assertions that touch the filesystem
Exceptions expect_raises, message and cause assertions, "must not raise"
Warnings expect_warns, counting, "must not warn"
Mocks Calls, arguments, counts — and the misspellings mock lets through
Types and enums Subclassing, protocols, members, flags

By what you are doing

Page Covers
Soft assertions Collect every failure in a block instead of stopping at the first
Matchers Assert the shape of a value when you cannot name all of it
Structural equivalence Compare two object graphs member by member
Counting occurrences exactly(3), at_least(1), and friends
Controlling output Print more of a value, or teach a message how your types read
Extending Your own assertions and your own subjects, with the same machinery

Reference

The assertion reference — every assertion on every subject, generated from the source and verified against it on every run. How the reference is built says what in it is derived, what is checked, and where the one gap is.


Concepts

Why it is built the way it is. Useful when you are deciding whether to adopt it, extending it, or debugging something surprising.

Page Question it answers
Design goals What this claims over pytest's assert rewriting, and what it does not
Typed dispatch How expect() picks a subject, and why the order is what it is
Failure messages The grammar every message follows, and the rules behind it
Performance What a passing assertion costs, and what a failing one is allowed to
Type-checker divergences Where pyright and mypy disagree, and what was decided

Should you depend on this?

The questions that decide it, answered in one place.

Maturity First release. The catalogue, exception and warning assertions, the difference engine and the extension API are complete and tested.
Stability The public surface is __all__, and it is what the reference documents. Until a 1.0, treat it as settled but not promised — changes are recorded in CHANGELOG.md.
Dependencies None, permanently. Python 3.13+.
Cost to your suite A passing assertion is a comparison and a return: nothing retained, no message built. Importing the package imports almost nothing. Performance says exactly what is measured and what is not.
Lock-in None. expect() is a function you import, its failures are ordinary AssertionErrors, and it mixes freely with assert in the same file. There is no plugin, no fixture and no base class.
When it does not fit Use a plain assert. Design goals is explicit about what this does not claim, and Migrating opens by saying where a bare assert is still the better call.
Escape hatches matches(predicate) for a condition with no assertion; satisfies for nested ones; your own assertions with the same machinery; .subject to leave the library with a typed value.
Type checkers pyright and mypy, both strict, both green in CI on Python 3.13 and 3.14. Where they disagree, the disagreement is written down rather than designed around.
Are these docs true? Every Python example on every page is run by the test suite — the few that cannot are marked on the page, with the reason — and every failure message they quote is compared byte for byte against what really came back. The same examples are type-checked, by pyright alone. A page that drifts from the library fails the build.

esc

Nothing matches that. The reference is generated, so try an assertion name.

No results. The reference is generated, so try an assertion name.

↑↓ navigate↵ open30 pages, every example executed in CI