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.
Download files
Download the file for your platform. If you're not sure which to choose, learn more about installing packages.
Source Distribution
Built Distribution
Filter files by name, interpreter, ABI, and platform.
If you're not sure about the file name format, learn more about wheel file names.
Copy a direct link to the current filters
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
| Algorithm | Hash digest | |
|---|---|---|
| SHA256 |
ec246d00d25b0297d29a2a5f36b8059cf0bf10086f5216b84b04e00b49ef62f6
|
|
| MD5 |
f18643047c3eaac01c46fb074a78a0ca
|
|
| BLAKE2b-256 |
da21f9cda002ad468613bf78f771c0df960a2fd7b9d3c5945d1943aece9b7bfa
|
Provenance
The following attestation bundles were made for lovely_assertions-0.2.0.tar.gz:
Publisher:
release.yml on lovely-assertions/lovely-assertions
-
Statement:
-
Statement type:
https://in-toto.io/Statement/v1 -
Predicate type:
https://docs.pypi.org/attestations/publish/v1 -
Subject name:
lovely_assertions-0.2.0.tar.gz -
Subject digest:
ec246d00d25b0297d29a2a5f36b8059cf0bf10086f5216b84b04e00b49ef62f6 - Sigstore transparency entry: 2674901099
- Sigstore integration time:
-
Permalink:
lovely-assertions/lovely-assertions@be6892f073809d1783283185b3372be9a08f0724 -
Branch / Tag:
refs/tags/v0.2.0 - Owner: https://github.com/lovely-assertions
-
Access:
public
-
Token Issuer:
https://token.actions.githubusercontent.com -
Runner Environment:
github-hosted -
Publication workflow:
release.yml@be6892f073809d1783283185b3372be9a08f0724 -
Trigger Event:
push
-
Statement type:
File details
Details for the file lovely_assertions-0.2.0-py3-none-any.whl.
File metadata
- Download URL: lovely_assertions-0.2.0-py3-none-any.whl
- Upload date:
- Size: 525.1 kB
- Tags: Python 3
- Uploaded using Trusted Publishing? Yes
- Uploaded via:
twine/7.0.0 CPython/3.13.14
File hashes
| Algorithm | Hash digest | |
|---|---|---|
| SHA256 |
3bc7a04d61ae0e291e0cf60b1604eb478676767957ce0938c13e8b01ada85a9c
|
|
| MD5 |
a6b5482eb8f7566218f0d80115c54edc
|
|
| BLAKE2b-256 |
d824358b930124fd006cf7a02b1043d3f92fdb25bbf6ff61d9a1dba8b2001913
|
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
-
Statement:
-
Statement type:
https://in-toto.io/Statement/v1 -
Predicate type:
https://docs.pypi.org/attestations/publish/v1 -
Subject name:
lovely_assertions-0.2.0-py3-none-any.whl -
Subject digest:
3bc7a04d61ae0e291e0cf60b1604eb478676767957ce0938c13e8b01ada85a9c - Sigstore transparency entry: 2674901246
- Sigstore integration time:
-
Permalink:
lovely-assertions/lovely-assertions@be6892f073809d1783283185b3372be9a08f0724 -
Branch / Tag:
refs/tags/v0.2.0 - Owner: https://github.com/lovely-assertions
-
Access:
public
-
Token Issuer:
https://token.actions.githubusercontent.com -
Runner Environment:
github-hosted -
Publication workflow:
release.yml@be6892f073809d1783283185b3372be9a08f0724 -
Trigger Event:
push
-
Statement type: