Checking if dataclass objects are equal to other objects is SLOW. That is, the implementation of the dunder __eq__ method in Python dataclasses causes dramatic and unexpected performance issues. Let me explain.
What is a “dunder __eq__” method
Equality of objects in Python is complicated, and should make math enthusiasts shudder. It’s not necessarily transitive, symmetric, or even reflexive.
Some objects are equal to all other objects. Some objects are equal to no objects, including themselves. An object A may be equal to object B while object B is not equal to object A. Object A may simultaneously be equal and not equal to object B.
How does this work?
We’ll do a quick deep dive (shallow dive?) into Python’s data model. A Python class has special methods and variables, usually hidden from the programmer’s view, with two underscores on each side of the name. They are commonly called “magic” or “dunder” (double underscore). If you’ve ever seen the following line of code, it’s accessing a dunder variable assigned to each module and checking if it’s the module initially being executed :
if __name__ == "__main__":
In CPython – the standard python interpreter available at python.org – some dunder methods are just wrappers around C functions. However, when it comes to equality checking, we are dealing with special dunder methods called “rich comparison methods.” If we check the section of the Python docs that describes the data model, we see that a double equal sign equality check is actually just a call to the left object’s dunder method __eq__. That is,
a == b
Results in a call to
a.__eq__(b)
This __eq__ method can be overridden to obtain all of the ridiculous results mentioned above. For example, no object of the NotEqual class is equal to itself (StackOverflow source)
class NotEqual: def __eq__(self, other): return False
The Danger of Dataclasses
In Python, a dataclass is a class designed to hold data. The @dataclass decorator automatically creates many dunder methods for a dataclass. However, the default __eq__ implementation is written in Python, not in C. This has implications for speed, as seen below.

I created large lists of four types of objects: Normal1 and Normal10 – normal classes with 1 and 10 fields respectively, and Data1 and Data10 – dataclasses with 1 and 10 fields respectively. The graph above shows the total time to run an equality check on each object in a list with each object in another list.
Why the dramatic speed difference? Equality checking for normal classes is done entirely in C, but dataclass equality checking is done in Python. As described in PEP 557, the equality check is done by first checking if the objects are of the same class. If they are not, the check returns NotImplemented. If they are of the same class, every value in both dataclasses are compared pairwise.
This doesn’t fully explain the graph above. Shouldn’t the code below. result in a call to the Normal1.__eq__() dunder method?
x = Normal1(1)y = Data1(1)x == y
This method is implemented in fast C, so why is this check slow? Returning to the Python data model:
A rich comparison method may return the singleton
NotImplementedif it does not implement the operation for a given pair of arguments. […]There are no swapped-argument versions of these methods (to be used when the left argument does not support the operation but the right argument does); rather,
__lt__()and__gt__()are each other’s reflection,__le__()and__ge__()are each other’s reflection, and__eq__()and__ne__()are their own reflection.
Ah ha! So the fast C equality check from the normal class on the left hand side sees that these objects are different types and it returns NotImplemented. The fallback is to call the slow Python implementation from the dataclass on the right hand side. Thus, it doesn’t matter which object is on which side; either choice is slow if a dataclass object is involved.
Now, why is the comparison between Data1 and Data10 the slowest? Well, this is the only case where a slow pure-Python __eq__ method is called twice from different classes!
Shouldn’t that mean that the Data40 and Data1 equality check is only slower than a DataN and DataN equality check for small values of N, because calling the pure Python __eq__ method twice is a constant time overhead?
Yes, yes it does

(Overall times are larger because I added many more objects to each list to highlight the difference)


Leave a Reply