datarekha
Python Medium Asked at GoogleAsked at MetaAsked at AmazonAsked at Stripe

What is the contract between `__eq__` and `__hash__` in Python, and what breaks when you define only one?

The short answer

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.

Learn it properly Dunder Methods

Keep practising

All Python questions

Explore further

Skip to content