v0.2.0 · python 3.13+ · zero dependencies

Your tests will fail. They may as well be lovely about it.

A zero-dependency Python assertion library for pytest and unittest. expect() offers only what applies to your value's type, narrowing survives the chain, and a failure turns up as a sentence rather than a shrug.

Read the docs
from lovely_assertions import expect

expect("lovely-assertions").starts_with("love").and_.contains("assertions")

compared with a bare pytest assert

pytest hands you two dicts.
This hands you the bug.

what you write today

assert "levitation" in spellbook
assert 'levitation' in {'levitatoin': 3, 'fireball': 1}

what you could write

expect(spellbook).contains_key("levitation")
Expected spellbook to contain key 'levitation' (did you mean 'levitatoin'?), but the keys were ['levitatoin', 'fireball'].

A missing key and a key holding the wrong value are two different bugs. The message picks one — and spots your typo on the way past.

Typed discoverability, real narrowing, and failure messages that explain themselves.

1

Only what applies

A str subject has no is_positive. Not hidden — absent, and your checker says so before you run anything.

expect(spell).is_lower()
expect(wishes).has_length(3)
2

Narrowing that sticks

The subject a chain returns is re-typed, statically — to pyright and to mypy alike. No cast anywhere.

found = expect(raw).is_not_none()
hero = found.subject
# hero: str, no cast, no shrug
3

Sentences, not diffs

Which value, what was required, what it actually held — plus a bounded difference block when the value is composite.

Expected quest_order to be sorted, but 'rescue'
at index 1 came after 'wedding':
['wedding', 'rescue', 'feast'].

AI agents read these tests too

An AI agent can only debug what the test run printed.

A diff of two dictionaries leaves it to guess which of them is wrong. A sentence naming the value, the requirement and what it actually held tells it which line to edit.

intent is in the call

No operator to reverse-engineer

is_sorted() says what is required. An agent reading the file knows the constraint without inferring it from a comparison and a comment.

wrong calls do not compile

A typed catalogue, not a guess

A str subject has no is_positive. An invented assertion is a type error in the same pass, not a red run twenty seconds later.

failures fit in context

Four hundred characters, not sixty thousand

Difference blocks are bounded. Comparing two five-thousand-element lists does not spend an agent's context window on data it cannot use.

one scope, three failures, one report

3 assertions failed:
  (1) Expected order_totals to be sorted, but 1 at index 1 came after 3: [3, 1, 2].
  (2) Expected server_config to contain key 'hostname' (did you mean 'host'?), but the keys were ['host'].
  (3) Expected config to contain entry 'port': 9090, but that key held 8080.

With soft_assertions(), one run reports every failure in the block. Three fixes in one pass instead of three red-green cycles — which is the difference between an agent finishing the job and an agent asking you to run the tests again.

One line. No plugin, no fixture, no base class.

Assertions belong with your tests, so a dev dependency is where it goes — and it will never grow a runtime dependency of its own.

$ uv add --dev lovely-assertions

expect("unicorn").contains("corn")✓ passes

  • Zero runtime dependencies. Permanently.
  • A passing assertion is a comparison and a return self.
  • pyright and mypy, both strict, both green.
  • Every failure message in the docs is real output.
  • It does not narrow your variable, and says so.

Four pages, in order, and you are fluent.

Then the guides, by type and by task, and a reference generated from the source.