Equality and Hashing: __eq__ and __hash__
18 min · Intermediate
Need help? Ask a tutor on WhatsApp (opens in a new tab)By default, custom Python objects compare equality by memory identity (is). Overriding __eq__ allows objects to compare by value. But to store custom objects in sets or as dictionary keys, you must also understand how Python handles __hash__.
Default Identity vs Value Equality
Without __eq__, two distinct objects with identical data evaluate as unequal.
class Point: def __init__(self, x, y): self.x = x self.y = y
p1 = Point(1, 2)p2 = Point(1, 2)print(p1 == p2) # False! (Checks id(p1) == id(p2))To make p1 == p2 evaluate to True, we must implement __eq__.
Implementing __eq__
__eq__ is the special method behind ==. It decides if two objects are equal by value. Return NotImplemented for types it does not know.
__eq__ takes self and other. Always check if other is of a compatible type first!
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, self.y) == (other.x, other.y)Example 1 · Identity versus value
class Point: def __init__(self, x, y): self.x = x self.y = y
def __eq__(self, other): return (self.x, self.y) == (other.x, other.y)
a = Point(1, 2)b = Point(1, 2)print(a == b)print(a is b)print(a != Point(3, 4))True False True
What this code does
==now compares values;isstill compares the objects themselves.!=works without extra code: Python flips the result of__eq__.
Example 2 · Comparing with other types
class Coupon: def __init__(self, code): self.code = code
def __eq__(self, other): if not isinstance(other, Coupon): return NotImplemented return self.code == other.code
print(Coupon("SAVE10") == Coupon("SAVE10"))print(Coupon("SAVE10") == "SAVE10")print(Coupon("SAVE10") == 10)True False False
What this code does
NotImplementedtells Python to try the other side; if neither side knows, the answer isFalse.- The method never raises an error for a strange type.
Returning NotImplemented allows Python to check if the other operand knows how to compare with us before falling back to False.
Why __hash__ is Unset by __eq__
Hashable describes an object with a __hash__ number that never changes while equal objects share it. Only hashable objects can be set members or dict keys.
As soon as you define __eq__, Python automatically sets __hash__ = None, making instances unhashable! Attempting set([p1]) raises TypeError: unhashable type: 'Point'.
class Point: def __init__(self, x, y): self.x = x self.y = y
def __eq__(self, other): return (self.x, self.y) == (other.x, other.y)
try: {Point(1, 2)}except TypeError as error: print(error)- Why? If an object’s attributes can mutate, its hash code would change while sitting inside a hash table bucket, permanently corrupting the dictionary or set!
- Python protects you from this silent data corruption by marking mutable objects as unhashable by default.
Defining __eq__ without __hash__ makes the class unhashable on purpose. If attributes could change, the hash would change while the object sits in a set, and the set would be corrupted.
Implementing __hash__ for Immutable Value Objects
If your object is treated as immutable, implement __hash__ using a tuple of its key fields.
class Point: def __init__(self, x, y): self.x = x self.y = y
def __eq__(self, other): return (self.x, self.y) == (other.x, other.y)
def __hash__(self): return hash((self.x, self.y))
# Now instances can be added to sets and dict keys!point_set = {Point(1, 2), Point(1, 2)}print("Set size:", len(point_set)) # 1Example 3 · An ID that works as a dict key
class UserID: def __init__(self, value): self.value = value def __eq__(self, other): if isinstance(other, UserID): return self.value == other.value return NotImplemented def __hash__(self): return hash(self.value)
u1 = UserID(101)u2 = UserID(101)user_cache = {u1: "Aarav"}print("Cache lookup:", user_cache.get(u2))Cache lookup: Aarav
What this code does
u1 == u2is True, andhash(u1) == hash(u2).u2successfully finds the cached value stored underu1.
Example 4 · Equal objects need equal hashes
class Sku: def __init__(self, code, label): self.code = code self.label = label
def __eq__(self, other): return isinstance(other, Sku) and self.code == other.code
def __hash__(self): return hash((self.code, self.label))
stock = {Sku("A1", "red")}print(Sku("A1", "blue") == Sku("A1", "red"))print(Sku("A1", "blue") in stock)True False
What this code does
- The two objects are equal, yet the lookup fails: the hash uses
label, but equality does not. - Hash exactly the fields that
__eq__compares.
Fundamental rule: If a == b, then hash(a) == hash(b) must ALWAYS be true.
Next step
Check your understanding
11 quick questions