---
title: "lovely-assertions documentation"
description: "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."
url: "https://lovely-assertions.dev/docs/"
library: "lovely-assertions"
version: "0.2.0"
source: "https://github.com/lovely-assertions/lovely-assertions/blob/v0.2.0/docs/README.md"
updated: "2026-09-01"
---

# 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.

```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](https://lovely-assertions.dev/docs/concepts/typed-dispatch/).
- **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](https://lovely-assertions.dev/docs/concepts/failure-messages/).

---

## Start here

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

| # | Page                                                                                                 | What you get                                           |
| - | ---------------------------------------------------------------------------------------------------- | ------------------------------------------------------ |
| 1 | [Installation](https://lovely-assertions.dev/docs/getting-started/installation/)                     | Installed, imported, working                           |
| 2 | [Your first assertions](https://lovely-assertions.dev/docs/getting-started/first-assertions/)        | The shape of every assertion you will write            |
| 3 | [Reading a failure](https://lovely-assertions.dev/docs/getting-started/reading-failures/)            | How to get a message that ends the investigation       |
| 4 | [Chaining and narrowing](https://lovely-assertions.dev/docs/getting-started/chaining-and-narrowing/) | `and_`, `.subject`, and how types flow through a chain |

**Coming from plain `assert`, `assertpy`, or `unittest`?**
[Migrating](https://lovely-assertions.dev/docs/guides/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](https://lovely-assertions.dev/docs/guides/any-value/)             | Equality, identity, `None`, truthiness, types, predicates — available on every subject |
| [Strings](https://lovely-assertions.dev/docs/guides/strings/)                 | Containment, prefixes, regex, wildcards, character classes                             |
| [Numbers and booleans](https://lovely-assertions.dev/docs/guides/numbers/)    | Comparisons, ranges, sign, tolerance, `nan`/`inf`, `implies`                           |
| [Collections](https://lovely-assertions.dev/docs/guides/collections/)         | Membership, length, set relations, duplicates, per-item checks                         |
| [Sequences](https://lovely-assertions.dev/docs/guides/sequences/)             | Order, position, sorting, element-by-element comparison                                |
| [Mappings](https://lovely-assertions.dev/docs/guides/mappings/)               | Keys, values, entries — and why the difference shows up in the message                 |
| [Dates and times](https://lovely-assertions.dev/docs/guides/dates-and-times/) | Comparison, tolerance, time zones, durations                                           |
| [Paths](https://lovely-assertions.dev/docs/guides/paths/)                     | Pure path shape, and assertions that touch the filesystem                              |
| [Exceptions](https://lovely-assertions.dev/docs/guides/exceptions/)           | `expect_raises`, message and cause assertions, "must not raise"                        |
| [Warnings](https://lovely-assertions.dev/docs/guides/warnings/)               | `expect_warns`, counting, "must not warn"                                              |
| [Mocks](https://lovely-assertions.dev/docs/guides/mocks/)                     | Calls, arguments, counts — and the misspellings `mock` lets through                    |
| [Types and enums](https://lovely-assertions.dev/docs/guides/types-and-enums/) | Subclassing, protocols, members, flags                                                 |

### By what you are doing

| Page                                                                                        | Covers                                                             |
| ------------------------------------------------------------------------------------------- | ------------------------------------------------------------------ |
| [Soft assertions](https://lovely-assertions.dev/docs/guides/soft-assertions/)               | Collect every failure in a block instead of stopping at the first  |
| [Matchers](https://lovely-assertions.dev/docs/guides/matchers/)                             | Assert the shape of a value when you cannot name all of it         |
| [Structural equivalence](https://lovely-assertions.dev/docs/guides/structural-equivalence/) | Compare two object graphs member by member                         |
| [Counting occurrences](https://lovely-assertions.dev/docs/guides/occurrences/)              | `exactly(3)`, `at_least(1)`, and friends                           |
| [Controlling output](https://lovely-assertions.dev/docs/guides/controlling-output/)         | Print more of a value, or teach a message how your types read      |
| [Extending](https://lovely-assertions.dev/docs/guides/extending/)                           | Your own assertions and your own subjects, with the same machinery |

---

## Reference

[The assertion reference](https://lovely-assertions.dev/docs/reference/assertions/) — every assertion on every
subject, generated from the source and verified against it on every run.
[How the reference is built](https://lovely-assertions.dev/docs/reference/) 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](https://lovely-assertions.dev/docs/concepts/design-goals/)                   | What this claims over pytest's `assert` rewriting, and what it does not |
| [Typed dispatch](https://lovely-assertions.dev/docs/concepts/typed-dispatch/)               | How `expect()` picks a subject, and why the order is what it is         |
| [Failure messages](https://lovely-assertions.dev/docs/concepts/failure-messages/)           | The grammar every message follows, and the rules behind it              |
| [Performance](https://lovely-assertions.dev/docs/concepts/performance/)                     | What a passing assertion costs, and what a failing one is allowed to    |
| [Type-checker divergences](https://lovely-assertions.dev/docs/concepts/typing-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](https://lovely-assertions.dev/docs/reference/assertions/) documents. Until a 1.0, treat it as settled but not promised — changes are recorded in [CHANGELOG.md](https://lovely-assertions.dev/changelog/).                                                           |
| **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](https://lovely-assertions.dev/docs/concepts/performance/) says exactly what is measured and what is not.                                                                           |
| **Lock-in**              | None. `expect()` is a function you import, its failures are ordinary `AssertionError`s, 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](https://lovely-assertions.dev/docs/concepts/design-goals/) is explicit about what this does *not* claim, and [Migrating](https://lovely-assertions.dev/docs/guides/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](https://lovely-assertions.dev/docs/guides/extending/) 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](https://lovely-assertions.dev/docs/concepts/typing-divergences/) 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. |
