Python __eq__ Operator Overloading
python **eq** operator overloading: Learn how to override __eq__ in Python to implement custom equality, handle type mismatches, and keep objects hashable.
When you use == on two instances of a custom class in Python, the default behavior is identity comparison: two objects are equal only if they are the same object. For many classes, that is not the semantics you want. Overriding __eq__ changes how == behaves for your type. This is part of python **eq** operator overloading, and it is one of the most common dunder methods developers implement.
The Default Behavior of ==
Without an __eq__ override, == falls back to is. That means two distinct instances with identical attribute values are not equal. For a Point class with x and y, Point(1, 2) == Point(1, 2) evaluates to False. This is often surprising when you expect value semantics.
The default __eq__ is inherited from object, and it compares object identity. If you do not override it, a == b is equivalent to a is b. This is fine for classes that represent unique entities, like database rows or connection objects, but wrong for value objects like coordinates, money amounts, or configuration settings.
Implementing eq for a Custom Class
To give your class value-based equality, define __eq__ with two arguments: self and other. The method should return True when the two objects are logically equal, and False otherwise. Here is a minimal implementation for a Point class:
class Point: def __init__(self, x, y): self.x = x self.y = y def __eq__(self, other): if not isinstance(other, Point): return NotImplemented return self.x == other.x and self.y == other.y
The isinstance check is important. If other is not a Point, you should not try to access its x and y attributes. Returning NotImplemented tells Python to try the reflected operation on the other operand, or fall back to the default behavior. This leads to symmetric equality: Point(1, 2) == something_else will not raise an AttributeError.
Returning NotImplemented for Unsupported Types
NotImplemented is a special singleton that signals the operation is not supported for the given operand type. When you return NotImplemented from __eq__, Python will try the other operand's __eq__ method. If that also returns NotImplemented, Python falls back to identity comparison and returns False.
This mechanism is essential for correct behavior when comparing objects of different types. For example, comparing a Point to a tuple should return False rather than raising an exception. Without the isinstance guard, you would get an AttributeError when trying to access other.x. Returning NotImplemented avoids that.
The Relationship Between eq and hash
Defining __eq__ in a class automatically sets __hash__ to None unless you also define it. This is a safety measure: if two objects are equal, they must have the same hash value. If you allow a mutable object to be used as a dictionary key, its hash could change, breaking the dictionary's invariants. Python prevents this by making unhashable any class that defines __eq__ without __hash__.
If your objects are immutable and you want them to be usable in sets and as dictionary keys, you must define __hash__. The rule is that equal objects must produce the same hash. For a Point, you can combine the hashes of the attributes:
class Point: def __init__(self, x, y): self.x = x self.y = y def __eq__(self, other): if not isinstance(other, Point): return NotImplemented return self.x == other.x and self.y == other.y def __hash__(self): return hash((self.x, self.y))
If you do not need your objects to be hashable, you can leave __hash__ as None and rely on identity for dictionary keys. But if you plan to store instances in sets or use them as keys, you must implement __hash__ consistently.
Type Checking: isinstance vs Exact Type
The isinstance check in __eq__ allows subclasses to be considered equal to the base class. For example, if you have a ColoredPoint subclass of Point, ColoredPoint(1, 2, 'red') == Point(1, 2) would return True because the ColoredPoint is an instance of Point. This may or may not be what you want.
If equality should only hold for the exact same type, use type(self) is type(other) instead of isinstance. This is stricter and prevents cross-type equality. The choice depends on your domain model. For value objects, exact type equality is often safer because it avoids subtle bugs where a subclass with extra attributes is considered equal to a base instance.
Consider the tradeoff: isinstance allows polymorphic equality, which can be useful in some hierarchies, but it can also violate the symmetric property if the subclass overrides __eq__ differently. A common pattern is to check type(self) is type(other) to ensure symmetry and consistency.
Performance and Runtime Cost of Equality Checks
__eq__ is called every time == is evaluated, and it is also used internally by sets and dictionaries to resolve hash collisions. This means the method can be a hot path in code that does a lot of comparisons. Keep it cheap: avoid expensive computations, I/O, or complex logic inside __eq__. The implementation should only compare the fields that determine logical equality.
For example, if your class has a large list of attributes, comparing all of them may be slower than comparing a single unique identifier. If you have a class that represents a database record, comparing primary keys is usually sufficient and much faster than comparing every column. The key is to understand what defines equality for your domain and implement the minimal comparison that preserves correctness.
Another performance consideration is that __eq__ should not have side effects. It must be a pure function: given the same two objects, it should always return the same result. If it modifies state or depends on external conditions, you will get unpredictable behavior in sets and dictionaries.
Common Pitfalls and Edge Cases
One common mistake is forgetting to handle None. If you compare point == None, the isinstance check will return NotImplemented, and Python will fall back to False. That is correct, but you might want to explicitly handle None if your class can be equal to None in some domain-specific way. Usually it should not.
Another pitfall is mutability. If you define __eq__ and __hash__ on a mutable class, you can break invariants. For example, if you have a list of points and you change the coordinates of a point that is already in a set, its hash changes, and the set becomes corrupted. The safest approach is to make value objects immutable by using @dataclass(frozen=True) or by not exposing setters.
When using @dataclass, Python automatically generates __eq__ and __hash__ based on fields. This is a convenient way to get value semantics without writing boilerplate. However, you still need to understand the underlying behavior if you customize fields or need special equality logic.
Maintaining Symmetry and Consistency
A well-designed __eq__ should be symmetric, transitive, and reflexive. Symmetry means a == b and b == a give the same result. Transitivity means if a == b and b == c, then a == c. Reflexivity means a == a is always True. Returning NotImplemented for unsupported types helps maintain symmetry because Python will try the other operand's method.
Consistency with __hash__ is also critical. If two objects are equal, they must have the same hash. Violating this rule will cause subtle bugs in dictionaries and sets. A common way to ensure consistency is to derive the hash from the same fields used in __eq__. For example, hash((self.x, self.y)) matches the equality check on x and y.
If you find that your equality logic is getting complex, consider using a @dataclass with eq=True (the default) and frozen=True to get immutable, hashable value objects. This reduces the risk of mistakes and makes the intent clear.