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.
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 astrproposesstarts_with; it does not proposeis_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().subjectis astrto both pyright and mypy — not anobject, 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. |