__eq__ vs __hash__
What __eq__ is
__eq__(self, other) defines the == operator. When you write a == b, Python calls a.__eq__(b); if that returns NotImplemented, it tries b.__eq__(a). The default (object.__eq__) compares identity — same object in memory — which is why two freshly-built User(1) instances are not equal by default.
class A: pass
A() == A() # False — default __eq__ is identity
[1, 2] == [1, 2] # True — list.__eq__ compares element-wise
!= derives from __eq__ automatically (negation), so you only override __eq__.
What __hash__ is
__hash__(self) returns an integer used by dict, set, and frozenset to bucket the object. Hashing is what makes lookup O(1) average — Python computes hash(key), jumps to the bucket, then uses __eq__ to confirm a match.
hash("abc") # some int, stable within a process
hash([1, 2]) # TypeError — lists are mutable, not hashable
{1, 2, 3} # set uses hash() of each element
{"a": 1}["a"] # dict computes hash("a") to find the bucket
The default (object.__hash__) returns a value derived from id(self) — unique per instance, consistent with the default identity-based __eq__.
The contract
The two methods have a contract you can’t break: if a == b, then hash(a) == hash(b). Violate it and dicts/sets corrupt silently.
a == b → hash(a) == hash(b)
The reverse is not required. Two unequal objects may share a hash (a hash collision); the dict/set then falls back to == to disambiguate. But equal objects with different hashes break lookup — they land in different buckets and the second insert silently shadows the first.
What Python does by default
object.__eq__is identity (a is b).object.__hash__isid(a)(well, derived from it).- Both honor the contract:
a is bimpliesid(a) == id(b).
So the default is safe but coarse — you can’t put two different User instances representing the same DB row into a set and have it dedupe.
Override __eq__ and __hash__ is killed
If you only override __eq__, Python sets __hash__ = None automatically. The class becomes unhashable.
class User:
def __init__(self, id):
self.id = id
def __eq__(self, other):
return isinstance(other, User) and self.id == other.id
u = User(1)
hash(u) # TypeError: unhashable type: 'User'
{u} # TypeError
This is a feature, not a bug. Python forces you to think: if I’m changing equality, what should the hash be?
The fix — define both
class User:
def __init__(self, id):
self.id = id
def __eq__(self, other):
return isinstance(other, User) and self.id == other.id
def __hash__(self):
return hash(self.id)
assert User(1) == User(1)
assert hash(User(1)) == hash(User(1))
{User(1), User(1)} # {User(1)} — deduplicated
Hash whatever participates in equality — same fields, same order.
Mutable objects shouldn’t be hashable
If __hash__ depends on mutable state, mutating after insertion breaks the dict.
class Bag:
def __init__(self):
self.items = []
def __eq__(self, other):
return isinstance(other, Bag) and self.items == other.items
def __hash__(self):
return hash(tuple(self.items)) # depends on mutable list
b = Bag()
s = {b}
b.items.append(1) # mutated
b in s # False — hash changed, can't find it
The dict located b at its old hash bucket; lookup now computes the new hash and looks in the wrong bucket. The object is “lost” inside the set.
Rule: hashable objects should be effectively immutable. Either don’t make mutable types hashable (__hash__ = None), or hash only fields you guarantee not to mutate (often: a stable ID).
dataclasses and the eq/hash interaction
@dataclass generates __eq__ by default, which kills __hash__. To opt in:
from dataclasses import dataclass
@dataclass(frozen=True) # immutable + hashable
class Point:
x: int
y: int
@dataclass(eq=True, unsafe_hash=True) # mutable + hashable (you take responsibility)
class Mutable:
x: int
@dataclass(eq=False) # no __eq__ override → default identity hash
class Identity:
x: int
@dataclass flags |
__eq__ |
__hash__ |
|---|---|---|
| default | generated by-fields | None (unhashable) |
frozen=True |
generated by-fields | generated by-fields |
eq=False |
default (identity) | default (id-based) |
unsafe_hash=True |
generated by-fields | generated by-fields, mutable allowed |
frozen=True is the right choice almost always. unsafe_hash=True is for cases where you’ve guaranteed (somehow) the hashed fields don’t change.
Why @dataclass(eq=True) (the default) kills __hash__
The dataclass decorator follows the same rule the language enforces: if you change equality, hashability is your responsibility to re-establish. Defaults reasoning:
@dataclassgenerates__eq__that compares all fields.- Instances are mutable (you can assign to
obj.x = ...after creation). - If
__hash__were also generated by-fields, mutating any field would change the object’s hash mid-life — violating “hash must not change for the lifetime of the object.”
So Python sets __hash__ = None. You then have three explicit choices:
frozen=True→ instances become immutable; safe to hash by fields.unsafe_hash=True→ “I promise these fields won’t change after I put this in a dict.” Caller’s responsibility.eq=False→ fall back to identity-based__eq__and__hash__(defaultobjectbehavior); two equal-by-fields instances are no longer equal.
Same mechanism, no magic — @dataclass is just enforcing the contract one decorator-deep.
Inheritance and hash
If a subclass overrides __eq__, Python sets __hash__ to None on the subclass — even if the parent had a working __hash__.
class A:
def __hash__(self):
return 42
def __eq__(self, other):
return True
class B(A):
def __eq__(self, other):
return isinstance(other, B)
hash(A()) # 42
hash(B()) # TypeError
To inherit the parent’s hash explicitly:
class B(A):
def __eq__(self, other): ...
__hash__ = A.__hash__
Common patterns
ID-based equality (entity in a domain model):
class Order:
def __init__(self, id, items):
self.id = id
self.items = items
def __eq__(self, other):
return isinstance(other, Order) and self.id == other.id
def __hash__(self):
return hash(self.id)
Two Order objects represent the same order if they have the same ID, regardless of items. Items can change without breaking dict lookups.
Value-based equality (value object):
@dataclass(frozen=True)
class Money:
amount: int
currency: str
Two Money instances are equal iff all fields match. Frozen → safe to use as dict key.
Disable hashing on a mutable equality:
class Cart:
def __eq__(self, other):
return isinstance(other, Cart) and self.items == other.items
__hash__ = None # explicitly unhashable; can't end up as a dict key by accident
What to put in hash
def __hash__(self):
return hash((self.id, self.kind)) # tuple of fields
hash(tuple_of_fields) is the canonical idiom — Python’s tuple hashing combines the field hashes well. Avoid arithmetic combinations like self.x ^ self.y — collisions are easier to construct, performance is no better.
Interview angle
- Q: “What’s the contract between
__eq__and__hash__?” —a == bimplieshash(a) == hash(b). - Q: “What happens if you override
__eq__and not__hash__?” — Python sets__hash__ = None; class becomes unhashable. - Follow-up: “Why shouldn’t mutable objects be hashable?” — hash depends on state; mutation after insertion breaks lookup.
- Follow-up: “How do dataclasses handle this?” —
@dataclass(frozen=True)for value objects (immutable + hashable); default kills__hash__because it generates__eq__.
See 18_dict_hashable_objects.md for hashable types in general, tricky_questions/03_is_vs_equals_interning.md for is vs == semantics, tricky_questions/23_empty_class_as_key.md for the default-hash gotcha.