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.

Download files

Download the file for your platform. If you're not sure which to choose, learn more about installing packages.

Source Distribution

lovely_assertions-0.2.0.tar.gz (851.3 kB view details)

Uploaded Source

Built Distribution

If you're not sure about the file name format, learn more about wheel file names.

lovely_assertions-0.2.0-py3-none-any.whl (525.1 kB view details)

Uploaded Python 3

File details

Details for the file lovely_assertions-0.2.0.tar.gz.

File metadata

  • Download URL: lovely_assertions-0.2.0.tar.gz
  • Upload date:
  • Size: 851.3 kB
  • Tags: Source
  • Uploaded using Trusted Publishing? Yes
  • Uploaded via: twine/7.0.0 CPython/3.13.14

File hashes

Hashes for lovely_assertions-0.2.0.tar.gz
Algorithm Hash digest
SHA256 ec246d00d25b0297d29a2a5f36b8059cf0bf10086f5216b84b04e00b49ef62f6
MD5 f18643047c3eaac01c46fb074a78a0ca
BLAKE2b-256 da21f9cda002ad468613bf78f771c0df960a2fd7b9d3c5945d1943aece9b7bfa

See more details on using hashes here.

Provenance

The following attestation bundles were made for lovely_assertions-0.2.0.tar.gz:

Publisher: release.yml on lovely-assertions/lovely-assertions

Attestations: Values shown here reflect the state when the release was signed and may no longer be current.

File details

Details for the file lovely_assertions-0.2.0-py3-none-any.whl.

File metadata

File hashes

Hashes for lovely_assertions-0.2.0-py3-none-any.whl
Algorithm Hash digest
SHA256 3bc7a04d61ae0e291e0cf60b1604eb478676767957ce0938c13e8b01ada85a9c
MD5 a6b5482eb8f7566218f0d80115c54edc
BLAKE2b-256 d824358b930124fd006cf7a02b1043d3f92fdb25bbf6ff61d9a1dba8b2001913

See more details on using hashes here.

Provenance

The following attestation bundles were made for lovely_assertions-0.2.0-py3-none-any.whl:

Publisher: release.yml on lovely-assertions/lovely-assertions

Attestations: Values shown here reflect the state when the release was signed and may no longer be current.

Release history Release notifications | RSS feed

This release

0.2.0 This release

2 files

0.1.0

2 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