Skip to main content

lovely-assertions

PyPI Python 3.13 and 3.14 CI Quality gate Coverage License: MPL 2.0

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

Fluent, strictly-typed assertions for Python tests. 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.

Install

uv add --dev lovely-assertions   # or: pip install lovely-assertions

Python 3.13 or newer, zero runtime dependencies, py.typed. Pre-1.0, so the API can still move; when it does, the reason is in CHANGELOG.md, generated from the commit log rather than written by hand.

A minute with it

from lovely_assertions import expect

expect("hello").starts_with("he")
expect([1, 2, 3]).contains_no_duplicates()
expect({"a": 1}).contains_key("a")
expect(3).is_positive()

Your editor knows there is no starts_with on that last line: expect(x). is the catalogue for the type of x, and nothing else. A check is also a narrowing, and both pyright and mypy agree about the result:

raw: str | None = "ada"

name: str = expect(raw).is_not_none().subject

And when something is wrong, you are told what:

from lovely_assertions import soft_assertions

with soft_assertions():
    expect([3, 1, 2], name="order_totals").is_sorted()
    expect({"host": "x"}, name="server_config").contains_key("hostname")
    expect({"port": 8080}, name="config").contains_entry("port", 9090)
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.

Three failures, one report — that is soft assertions. And look at the last two: the key is missing and the key holds something else are different bugs, and the sentence says which instead of leaving you to work it out.

Why not just assert

pytest already rewrites assert a == b into a passable diff, so the competition is pytest itself and the value has to be elsewhere. It is in three places, and breaking any one of them would leave this package with no reason to exist.

  • Typed discoverability. A str subject has no is_positive. Nothing that dispatches at runtime can offer that, and neither can a raw assert.
  • Real narrowing. The returned subject is re-typed, as above. The original variable is not: TypeIs reaches only a function's first argument, so rebind and the type is guaranteed. No Python assertion library does better, and this one writes the limitation down rather than implying otherwise.
  • Messages that explain. A composite value adds a difference block that stays bounded, so two five-thousand-element lists cost four hundred characters, not sixty thousand.

Also in the box

  • Exceptions, in the shape you already reach for: with expect_raises(ValueError) as caught:, then caught.with_message_containing(…). The wrong exception is chained onto the failure, so its traceback survives beside the message instead of replacing it.
  • Your own assertions, on the same machinery: subclass Expect[T], mark the methods @custom_assertion, and inherit subject naming, soft scopes, because and the whole catalogue. See the extension guide.

Documentation

Full documentation →

New here Installation · Your first assertions · Reading a failure
How do I assert…? The guides — by type, and by task
Every assertion The reference, generated from the source
Why it works this way Concepts — dispatch, messages, performance, typing
Coming from assert or assertpy Migrating

Every Python block on those pages is executed by the test suite, and every failure message they quote is compared against what the library actually produces. That includes this page.

Contributing

uv sync
uv run ruff check . && uv run ruff format --check . && uv run pyright && uv run mypy && uv run pytest

Every one of those must be green. CONTRIBUTING.md has the rest: what a change usually touches, what each CI gate proves, how a release is cut, and the terms a contribution arrives under. Taking part means agreeing to the code of conduct.

Security

Report a vulnerability privately through the Security tab, not as a public issue. SECURITY.md also documents what the library does on the failure path — it renders your values, and it reads the source line you wrote the assertion on — so the boundary is written down rather than discovered. Releases go out through PyPI Trusted Publishing, with no API token anywhere in this repository, and every artifact carries a signed build provenance attestation.

License

Mozilla Public License 2.0 — file-level copyleft. Use it in anything, commercial included, and nothing about your own code is affected. Modify these files and those files stay under this licence, which is the whole of the obligation: it reaches the files it came in, and no further.

Releases up to and including 0.1.0 were published under the MIT licence and remain so permanently for anyone holding them. The change applies from 0.2.0.

Metadata

Release files for lovely-assertions 0.3.0

For a detailed explanation of source distributions (sdists) and built distributions (wheels), please see the package formats documentation.

Source distribution (sdist)

Source distribution for lovely-assertions 0.3.0
File Size Uploaded
lovely_assertions-0.3.0.tar.gz 907.0 kB Details

Built distribution (wheel)

Table of built distributions (wheels) for lovely-assertions 0.3.0
File Interpreter ABI Platform
lovely_assertions-0.3.0-py3-none-any.whl Python 3 none any Details

Total release size: 1.5 MB

Release files / lovely_assertions-0.3.0.tar.gz

Download URL lovely_assertions-0.3.0.tar.gz
Size 907.0 kB
Tags Source
SHA-256 checksum
How to use checksums
ea59e5c6e131648ba8f29bb1c67abb4ead640cdf3e2928a8e5b8b09bccdad248
BLAKE2b-256 checksum
How to use checksums
24344984bb2bda3ebe2fdf6741f5b39b695274d75a134fb432bb8341c8dcd55c
Upload date
Uploaded using Trusted Publishing?
What is trusted publishing?
Yes
Uploaded via twine/7.0.0 CPython/3.13.14

Provenance

Provenance describes where a file came from. On PyPI, provenance is shared via attestations, which provide a verifiable record of the build or publishing details. View details, limitations and caveats.

PyPI Publish Attestation

PyPI verified that this artifact, at this checksum, originated from the publisher listed below.

Signed by GitHub Actions, verified by PyPI on Sep 21, 2026.

Transparency log

Release files / lovely_assertions-0.3.0-py3-none-any.whl

Download URL lovely_assertions-0.3.0-py3-none-any.whl
Size 558.5 kB
Tags Python 3
SHA-256 checksum
How to use checksums
d6e2a3827943e0f9389dab9568f5b43a9e46b41fdffba622190e1bf798546ac0
BLAKE2b-256 checksum
How to use checksums
f0c6bb6926460449d97f7a0724b6cbbddd5756c7ccc47e2037a1a02ec38844e6
Upload date
Uploaded using Trusted Publishing?
What is trusted publishing?
Yes
Uploaded via twine/7.0.0 CPython/3.13.14

Provenance

Provenance describes where a file came from. On PyPI, provenance is shared via attestations, which provide a verifiable record of the build or publishing details. View details, limitations and caveats.

PyPI Publish Attestation

PyPI verified that this artifact, at this checksum, originated from the publisher listed below.

Signed by GitHub Actions, verified by PyPI on Sep 21, 2026.

Transparency log

Release history Release notifications | RSS feed

This release

0.3.0 This release

2 release files

0.2.0

2 release files

0.1.0

2 release files

Anthropic, PBC Visionary sponsor Bloomberg Visionary sponsor Hudson River Trading Visionary sponsor Meta Visionary sponsor NVIDIA Visionary sponsor Microsoft Sustainability sponsor Depot Continuous Integration AWS Cloud computing and Security Sponsor Datadog Monitoring Fastly CDN Google Download Analytics Sentry Error logging StatusPage Status page