Why does defining `__eq__` on a class make its instances unhashable?
answer
- Two operations that must agree
- The class machinery poisons an attribute
- Buckets first, comparison second
- __hash__ becomes None, not missing
- frozen dataclass gets both back
basics
~20 sBecause Python sets hash to None on any class that defines eq without also defining hash. The inherited identity hash would disagree with your new equality, and equal objects must hash equally, so hashing is disabled until you decide what it should be.
solid answer
~40 sHashing has a contract: if `a == b` then `hash(a)` must equal `hash(b)`, because a `dict` or `set` finds a bucket by hash and only then compares candidates with `==`. `object.__hash__` is derived from identity and matches the default identity-based `__eq__`. As soon as a class body defines `__eq__` over fields, that pairing breaks, so the class machinery writes `__hash__ = None` into the new class, and any attempt to hash an instance raises `TypeError` — in a `set`, as a dict key, or through anything that memoises on arguments. You fix it deliberately: define `__hash__` over exactly the fields `__eq__` compares (safe only if they never change), let `@dataclass(frozen=True)` generate both, or restore identity hashing with `__hash__ = object.__hash__` when you accept that equal-but-distinct objects will not find each other.
code
python · 13 linesclass Money:
def __init__(self, cents):
self.cents = cents
def __eq__(self, other):
return isinstance(other, Money) and self.cents == other.cents
print(Money.__hash__) # None
try:
{Money(100)}
except TypeError as exc:
print(exc)go deeper
Recognise the symptom: a TypeError naming your class as unhashable the first time an instance meets a set or a dict key, right after someone gave the class value equality. Know that the cause is the missing hash, not the container.
Explain the invariant that equal objects must hash equally, and that the class machinery writes hash = None to protect it. Be able to name all three fixes and say which fields a hand-written hash may use.
Show you can spot the dangerous fix: a hash over mutable or partial state passes the interpreter and corrupts lookups silently. Talk about immutability as the precondition for a key type, and about testing the contract explicitly.
Set the codebase policy: which domain types get value semantics and frozen construction, which stay identity-based entities, and how the boundary is enforced so a well-meaning equality addition cannot quietly break every cache keyed on those objects.
Python enforces one invariant on hashing: **objects that compare equal must have the same hash**. If `a == b` is true then `hash(a) == hash(b)` must be true. Sets and dictionaries depend on it. A lookup computes the hash of the key, jumps to a bucket, and only compares candidates in that bucket with `==`. If two equal objects land in different buckets, the container simply never sees the match: you store an entry under one object and fail to find it with an equal one, silently. Object creation is where Python defends the invariant. When a class body defines `__eq__` and does not define `__hash__`, the class machinery sets `__hash__` to `None` in the new class's namespace. `None` is not callable, and `hash()` looks the slot up on the type, so `hash(instance)` raises `TypeError`, and every operation that hashes — adding to a `set`, using the object as a dict key, putting it in a `frozenset`, calling `functools.lru_cache` on a function that takes it — raises the same error. Nothing is deleted or optimised away; the attribute is deliberately poisoned so that the failure is loud and immediate instead of a lost dictionary entry three modules downstream. The rationale follows from what the two defaults do. `object.__hash__` derives a value from the object's identity, and `object.__eq__` is identity too, so the pair is consistent by construction. The moment you redefine equality in terms of fields, the inherited identity hash stops agreeing with it: two distinct instances that you have declared equal would hash differently. Rather than let you ship that, Python removes hashing until you say what should happen. **Restoring hashability, in order of preference.** 1. *Make the type immutable and hash the same fields you compare.* Define `__hash__` returning `hash((self.field_a, self.field_b))` over exactly the tuple of fields your `__eq__` uses. Now the invariant holds by construction. Do this only if those fields never change after construction — a key that mutates while it sits in a dict lands in the wrong bucket and is unreachable, and the dict will not notice. 2. *Let the standard library write both.* A dataclass declared with `frozen=True` generates `__eq__` and a matching `__hash__` from the compared fields, and blocks attribute assignment so the hash cannot drift. A plain `@dataclass` (which generates `__eq__` and leaves the instance mutable) sets `__hash__` to `None`, exactly like a hand-written `__eq__` would. 3. *Keep identity hashing on purpose.* Writing `__hash__ = object.__hash__` in the class body restores the inherited identity hash. This is legitimate only when you understand the consequence: two objects that compare equal will usually hash differently, so the type is safe to put in a dict only if you never rely on an equal-but-distinct key finding the entry. Graph nodes and other identity-first types with a convenience `__eq__` sometimes take this route; most domain value types should not. **Inheritance follows the same rule.** The `__hash__ = None` assignment happens per class body, so a subclass that overrides `__eq__` becomes unhashable even if its base defined a perfectly good `__hash__`. If the subclass's equality is compatible, re-declare `__hash__ = Base.__hash__` explicitly; the explicitness is the point. **Two symptoms to recognise in an interview.** First, `TypeError` naming the type as unhashable the first time an instance meets a set or a dict key — the usual trigger is calling `set()` on a list of newly value-ified objects, or memoising a function on them. Second, and worse, no error at all: someone "fixed" the first symptom by adding a `__hash__` that hashes a subset of the compared fields, or by hashing mutable state. That version satisfies the interpreter and violates the contract on the data, producing duplicates in a set and lookups that miss intermittently, which is far harder to diagnose than the original `TypeError`. **Checking it yourself.** `SomeClass.__hash__` is `None` for the broken case — that single expression tells you whether a class was silently made unhashable, and it is the fastest thing to inspect when a type that used to work in a set stops working after someone added value equality to it. The complementary check is behavioural: build two equal instances and assert both `a == b` and `hash(a) == hash(b)`, then assert that `{a, b}` has exactly one element. Those three assertions encode the whole contract, and they are worth writing the day you give a class value semantics.
- Is it ever correct to write `__hash__ = object.__hash__` next to a custom `__eq__`?Yes, when the type is an entity rather than a value: you want identity hashing but a convenience equality. The consequence must be understood — two objects that compare equal will normally hash differently, so an equal-but-distinct key will not find an existing dict entry. Use it for graph nodes or session objects, not for money, coordinates or other value types.
- What goes wrong if you hash a field that can change after the object is stored in a dict?The entry becomes unreachable. The dict placed the key in a bucket computed from the old hash; after the mutation, lookups compute a different bucket and miss, while iteration still yields the entry. Nothing raises. That is why hashable types should be immutable in the fields they hash, and why `@dataclass(frozen=True)` blocks attribute assignment.
- Does a subclass inherit hashability if it overrides only `__eq__`?No. The rule is applied per class body, so a subclass that defines `__eq__` gets its own `__hash__ = None` even when the base class defined a working `__hash__`. If the subclass's equality is compatible with the base's hash, re-declare it explicitly, for example `__hash__ = Base.__hash__`.
A library files books by a shelf code computed from the title; if you redefine which books count as the same book but leave the old shelf code, two identical books sit on different shelves and neither search finds the other.
saying these in an interview costs you the question
- Thinks Python drops __hash__ to save memory
- Claims any class with __eq__ still works in a set
- Hashes mutable fields and calls the contract satisfied
- Says only sets fail, dict keys are fine
- Believes a frozen dataclass is unhashable
- Adds a __hash__ over fewer fields than __eq__ compares