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
strsubject has nois_positive. Nothing that dispatches at runtime can offer that, and neither can a rawassert. - Real narrowing. The returned subject is re-typed, as above. The original
variable is not:
TypeIsreaches 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:, thencaught.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,becauseand the whole catalogue. See the extension guide.
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)
| File | Size | Uploaded | |
|---|---|---|---|
| lovely_assertions-0.3.0.tar.gz | 907.0 kB | Details |
Built distribution (wheel)
| File | Interpreter | ABI | Platform | Reset |
|---|---|---|---|---|
| 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 logRelease 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