dirty-equals: turning the equality operator into an assertion language for tests
Doing dirty (but extremely useful) things with equals.
At a glance
- What is it?
- dirty-equals is a Python library that overloads __eq__ so a test can assert against a value it cannot know exactly, comparing a positive integer, a regex, a JSON payload or a timestamp close to now without writing the assertion out longhand.
- Who is it for?
- dirty-equals fits a Python team whose unit tests are mostly assertions on API responses and database rows, and who find those assertions verbose and brittle. It is a poor fit for production code, since the whole mechanism depends on the left operand being a plain value, and it is a poor fit if your test suite needs to explain failures to non-programmers, because a failure message points at an equality operator rather than a named expectation.
- Can I use it commercially?
- Yes. MIT is a permissive licence: you can use, modify and sell software built on it, as long as you keep its copyright and licence notices.
- Is it still maintained?
- Yes. The repository last received commits 74 days ago.
- What is it written in?
- Mainly Python, according to GitHub's language statistics.
Answers come from the project's GitHub data, last synced on October 23, 2026, and from our analysis. They are not legal advice.
Editorial analysis
The trick, and why it is called dirty
dirty-equals is a Python library that (mis)uses the __eq__ method to make Python code, generally unit tests, more declarative and therefore easier to read and write. The name and the tagline, doing dirty but extremely useful things with equals, are the author's own framing of a technique that is technically abusive and practically valuable.
The mechanism is that Python's equality operator is not symmetric. When you write a == b, Python first tries a.__eq__(b), and only if that returns NotImplemented does it try b.__eq__(a). dirty-equals uses that asymmetry: the left operand is the real value, the right operand is a special object describing a condition, and the comparison returns True or False rather than a bool for the object's own sake.
The trivial example makes the point without making it useful:
from dirty_equals import IsPositive
assert 1 == IsPositive
assert -2 == IsPositive # this will fail!The second line fails, which is the point. IsPositive is not a value, it is a description of a class of values, and the comparison asks whether the left operand is one of them.
What the library is for is the case the README calls the real one: writing unit tests for applications where you are commonly checking the response to API calls and the contents of a database. The library can be used in whatever context you like, but that is where it comes into its own.
The project is MIT licensed, published on PyPI as dirty-equals, authored by Samuel Colvin, and classified as Development Status 4 - Beta with a Typing :: Typed classifier. The last push was on 2026-08-10, with releases v0.11.0 from 2025-11-16, v0.10.0 from 2025-09-19 and v0.9.0 from 2025-01-11.
What a declarative assertion looks like in practice
The README shows the difference the library makes with a realistic example, where user_data is a dict returned from a database or API that the test wants to check.
from dirty_equals import IsJson, IsNow, IsPositiveInt, IsStr
...
# user_data is a dict returned from a database or API which we want to test
assert user_data == {
# we want to check that id is a positive int
'id': IsPositiveInt,
# we know avatar_file should be a string, but we need a regex as we don't know whole value
'avatar_file': IsStr(regex=r'/[a-z0-9\-]{10}/example\.png'),
# settings_json is JSON, but it's more robust to compare the value it encodes, not strings
'settings_json': IsJson({'theme': 'dark', 'language': 'en'}),
# created_ts is datetime, we don't know the exact value, but we know it should be close to now
'created_ts': IsNow(delta=3),
}Four different kinds of unknown are handled in one expression. The id is known to be a positive integer but its value is not known. The avatar_file is known to match a pattern but the whole value is not known. The settings_json is known to decode to a particular object, and comparing the decoded value rather than the string is more dependable than comparing text that happens to serialise the same way. The created_ts is a timestamp known to be within three seconds of now.
The README makes the point about what you would otherwise write: without dirty-equals you would have to compare individual fields and or modify some fields before comparison, and the test would not be declarative or as clear.
That is the whole value proposition. A test that says what it knows, rather than describing the procedure to extract and compare it, is both shorter and more likely to catch a change in the wrong field.
The catalogue of comparison types, and the boolean operators
The type list is where the library earns its name, and each entry corresponds to a kind of partial knowledge about a value.
For dictionaries there are two. IsPartialDict lets you compare a subset of a dictionary, so you assert the fields you care about and ignore the rest, and IsStrictDict lets you confirm the order in a dictionary, which is the opposite requirement and the one that catches a bug where a function is building a dict in the wrong order.
For sequences, IsList and IsTuple let you compare partial lists and tuples, with or without order constraints. So you can assert that a list contains three specific items in any order, or that a tuple starts with a particular value.
IsInstance lets you simply confirm the type of an object, without asserting anything about its contents.
And you can use the boolean operators | and & to combine multiple conditions, which the documentation covers under boolean logic. That composition is what makes these usable in a large assertion, since a condition on one field can be a conjunction of a type check and a value check.
The library also supports nesting any of these types inside any others, so a partial dict can contain a list matcher inside it, and a list matcher can contain a dict matcher.
The optional pydantic extra is the exception to the zero-dependency rule, and it is there because a pydantic model is a natural thing to compare against, since its fields are already the type you would otherwise have to describe by hand.
The build, the docs, and the mypy check that is not a mypy target
The repository is a small Python project with a modern toolchain, and the Makefile is where the conventions are visible.
The install target checks that uv and pre-commit are present, then runs uv sync with --frozen and --all-groups and installs the pre-commit hooks. The frozen flag means the lockfile is authoritative and the sync will not silently update it, which is the same discipline the pydantic-settings project uses, and both are by the same author.
The lint and format targets both go through ruff, one writing and one checking. The docs target builds the documentation with mkdocs under the strict flag, so a broken link fails the build.
The test target is the interesting one. It runs coverage with TZ=utc set, and then it runs a separate script, tests/mypy_checks.py, as a second step. The TZ is there because the library has time-based comparisons, IsNow among them, and a test that depends on the local timezone is a test that fails in some environments and not others. Pinning the timezone makes the comparison deterministic.
That mypy_checks.py script is not a mypy target in the normal sense. The separate mypy target runs mypy over dirty_equals and the same tests/mypy_checks.py file, so the file is both a test and a typing fixture. It is presumably a set of snippets whose type checking is asserted, which is the only way to check that a library's type annotations are usable from the outside, since mypy running over the library's own source cannot tell you whether a user can use the API in a typed way.
The all target chains lint, mypy, testcov and docs, so a full check covers formatting, typing, tests with coverage, and the documentation build. The clean target removes pycache directories, coverage data, caches, htmlcov, egg-info and the build directory. The help target parses the Makefile itself and prints the recipes with the comment text, so the help output is generated from the file rather than maintained separately.
Where the trick stops working
The first limitation is that this only works when the left operand is a plain value and the right operand is the matcher. Python tries the left operand's __eq__ first, so if you write IsPositive == 1 the order is wrong and the comparison does not do what you want. The convention is that the matcher goes on the right, which is also the way an assert reads naturally, but it is a rule the reader has to know.
The second limitation is the failure message. When an assertion fails, pytest shows you a diff of the two operands, and the right operand is an object with a description of a condition rather than a value. It is readable, but it is not the same as a named expectation with a message, and for a team where a non-programmer reads the test report, the difference matters.
The third limitation is that the semantics live in the comparison, which means the comparison is doing work that usually belongs somewhere else. A test that says the value is a positive int is checking a property, and a property check can be done more explicitly. dirty-equals is at its best when the value is genuinely unknown, an id, a timestamp, a substring, and it is at its worst when the value is known and you are using a matcher to work around a clumsy comparison.
The fourth limitation is the version line. The project is at 0.11.x, classified as Beta, with the last release in November 2025. The 0.x numbering means the API can change in a minor release, which for a test library is a smaller problem than for a runtime library, since your tests break and you fix them, but it is still something to weigh when a large suite depends on it.
On maintenance, the repository is not archived, the last push was on 2026-08-10, and the documentation is on dirty-equals.helpmanual.io with the source in the repository and a docs/ directory and mkdocs.yml for the site.
Editorial conclusion
dirty-equals fits a Python team whose unit tests are mostly assertions on API responses and database rows, and who find those assertions verbose and brittle. It is a poor fit for production code, since the whole mechanism depends on the left operand being a plain value, and it is a poor fit if your test suite needs to explain failures to non-programmers, because a failure message points at an equality operator rather than a named expectation. Before adopting, install dirty-equals on Python 3.9 or newer, and use it only where the value is genuinely unknown, an id, a timestamp, a regex match, since that is the case where the comparison semantics are clearer than the code they replace.
Frequently asked questions
What is dirty-equals?
It is a Python library that misuses the __eq__ method to make Python code, generally unit tests, more declarative and easier to read and write. A special object on the right of an equality check describes a condition, so a test can assert against a value it does not know exactly.
How do I install dirty-equals?
Run pip install dirty-equals. The library requires Python 3.9 or newer, and it is published on PyPI as dirty-equals with MIT licensing.
What comparison types does dirty-equals provide?
IsPartialDict compares a subset of a dictionary, IsStrictDict confirms order in a dictionary, IsList and IsTuple compare partial sequences with or without order constraints, and IsInstance confirms the type of an object. Any of them can be nested inside any other, and the boolean operators | and & combine conditions.
Why does the dirty-equals test suite set TZ=utc?
Because the library has time-based comparisons such as IsNow, and a test that depends on the local timezone fails in some environments and not others. The Makefile test target runs coverage with TZ=utc set for that reason, and it also runs tests/mypy_checks.py as a separate step.
Official sources
Add this badge to your README
If you maintain this project, the badge below links readers to this analysis and shows its maintenance status from the daily GitHub snapshot. Paste the markdown into your README; add ?metric=license or ?metric=stars to the image URL for a different field.
[](https://hysenlabs.com/projects/samuelcolvin-dirty-equals)Was this useful?