What is the contract between `__eq__` and `__hash__` in Python, and what breaks when you define only one?
Objects that compare equal must have identical hash values. Python enforces this expectation by setting `__hash__` to `None` whenever you define `__eq__` without `__hash__`, making the object unhashable and ineligible as a dict key or set member. To restore hashability you must define both.
How to think about it
A hash table — a dict or a set — finds things in two steps: it hashes the key to pick a bucket, then it uses == to confirm the match inside that bucket. So if two objects compare equal but hash differently, the table looks in the wrong bucket and never finds the second one. That’s the contract the whole structure leans on: a == b must imply hash(a) == hash(b).
Python can’t check that you’ve honoured it — it can’t see inside your __eq__. But it enforces the half it can: the moment you define __eq__, Python assumes you’ve redefined what “equal” means and quietly sets __hash__ = None, making the object unhashable — unless you define __hash__ too.
A worked example
The reliable pattern is to hash a tuple of exactly the fields that define equality — tuple hashing is well-distributed and order-sensitive, which is just what you want:
class Point:
def __init__(self, x, y):
self.x = x
self.y = y
def __eq__(self, other):
if not isinstance(other, Point):
return NotImplemented # let the other type try
return self.x == other.x and self.y == other.y
def __hash__(self):
return hash((self.x, self.y)) # delegate to a tuple
def __repr__(self):
return f"Point({self.x}, {self.y})"
p1, p2, p3 = Point(1, 2), Point(1, 2), Point(3, 4)
print("p1 == p2 :", p1 == p2)
print("hash(p1)==hash(p2):", hash(p1) == hash(p2))
print("unique points :", sorted({p1, p2, p3}, key=lambda p: (p.x, p.y)))
# Define __eq__ but forget __hash__, and the object turns unhashable
class BrokenPoint:
def __init__(self, x, y):
self.x, self.y = x, y
def __eq__(self, other):
return self.x == other.x and self.y == other.y
try:
{BrokenPoint(1, 2)}
except TypeError as e:
print("BrokenPoint in set:", e)
p1 == p2 : True
hash(p1)==hash(p2): True
unique points : [Point(1, 2), Point(3, 4)]
BrokenPoint in set: unhashable type: 'BrokenPoint'
Two equal points hash the same, so a set collapses the three inputs down to two — the contract working for you. BrokenPoint defined only __eq__, so Python nulled its __hash__, and dropping one into a set raises straight away.
Inheriting from a hashable parent
The same trap springs on subclasses. Add __eq__ to a subclass of a hashable parent and Python still sets __hash__ = None — you have to opt back in by hand:
class NamedPoint(Point):
def __init__(self, x, y, label):
super().__init__(x, y)
self.label = label
def __eq__(self, other):
if not isinstance(other, NamedPoint):
return NotImplemented
return super().__eq__(other) and self.label == other.label
__hash__ = Point.__hash__ # explicitly re-enable hashing
Why NotImplemented, not False
When the other operand is an incompatible type, return NotImplemented rather than False. Python then gives the other object a chance to compare via its reflected method — which is how your type stays interoperable with third-party ones.