Why does defining `__eq__` make a class's instances unusable as dict keys?
answer
- Half a contract was just redefined
- The class attribute is set, not raised
- Check what the class attribute now is
- TypeError only at first container use
- One key tuple feeds both dunders
basics
~10 sWhen a class body defines eq without hash, Python sets hash to None in that class, so hash() raises TypeError: unhashable type. Define hash over the same fields to restore it.
solid answer
~40 sDefining `__eq__` changes what equality *means*, and the inherited `object.__hash__` — derived from `id()` — would then contradict it, because two objects that now compare equal would still hash differently. Rather than let that silently corrupt containers, Python sets `__hash__` to `None` in any class body that defines `__eq__` and not `__hash__`, which makes `hash()` raise `TypeError: unhashable type`. You see it the moment the instance is used in a `set`, as a `dict` key, inside a hashable `tuple`, or as an argument to a `functools.lru_cache`-decorated function. `SomeClass.__hash__ is None` confirms the diagnosis. The fix is to define `__hash__` returning `hash()` of the same tuple of fields your `__eq__` compares — or to leave the class unhashable on purpose, exactly as `list` and `dict` are.
code
python · 15 linesclass Point:
def __init__(self, x, y):
self.x, self.y = x, y
def __eq__(self, other):
if not isinstance(other, Point):
return NotImplemented
return (self.x, self.y) == (other.x, other.y)
print(Point.__hash__) # None
try:
{Point(1, 2): "first"}
except TypeError as exc:
print(exc) # cannot use 'Point' as a dict key (unhashable type: 'Point')go deeper
Recognise the symptom and its cause: TypeError: unhashable type after adding an __eq__, and the fix of also defining __hash__. Know that the class works fine until it meets a set or a dict.
Explain the mechanism precisely — the class body defining __eq__ without __hash__ gets __hash__ set to None at class creation — and write the standard recipe where one tuple of fields feeds both methods.
Argue the design decision: which classes deserve value equality at all, when leaving the type unhashable is the honest choice, and why restoring object.__hash__ under a value-based __eq__ is a defect rather than a fix.
Set the house rule. Value types that enter hash-based containers should be immutable by construction, and reviewers should treat any hand-written __eq__/__hash__ pair as a contract change needing a test that both survive de-duplication and lookup.
## The rule When a class body defines `__eq__` and does not define `__hash__`, Python sets that class's `__hash__` to `None`. This happens at class-creation time, not when you call anything, and it applies to the class where `__eq__` was written — a mixin that defines `__eq__` makes the mixin, and anything inheriting it without its own `__hash__`, unhashable. `None` is not a callable, so `hash(instance)` raises `TypeError: unhashable type: 'MyClass'`. The class is *unhashable*, exactly like `list`, `dict` and `set`. On 3.14 the message is fuller when a container is involved — using the object as a key reports `cannot use 'MyClass' as a dict key (unhashable type: 'MyClass')` — which is a useful tell, because it names the operation as well as the type. ## Why Python does that Hashing and equality are one contract, not two features. A hash-based container decides **where** to look using the hash, and only then uses `==` to pick the right entry among the candidates it finds there. So if two objects compare equal but hash differently, the container will look in the wrong place and never even run the `==` that would have matched. The moment you define a value-based `__eq__`, the inherited identity hash is wrong by exactly that standard: `Point(1, 2) == Point(1, 2)` is now `True`, but the two objects have different `id()` values and therefore different inherited hashes. Python's designers chose to break loudly at `hash()` time rather than let you insert into a `dict` and get silently unfindable keys. Read the `TypeError` as "you changed half of a contract; finish the job or opt out." ## Where the failure surfaces The class definition itself is fine; the error appears at first use: - `{obj}` or `some_set.add(obj)` - `d[obj] = value` or `obj in some_dict` - putting the object in a `tuple` that is then used as a key — a `tuple` is hashable only if every element is - calling a function decorated with `functools.lru_cache`, which builds its cache key from the arguments - `collections.Counter` over such objects, or any de-duplication that routes them through a `set` The diagnosis is one line: `MyClass.__hash__ is None`. ## The three legitimate responses **1. Define `__hash__` over the same fields.** The standard recipe is to build one tuple of the fields that define the value, and use it for both dunders: ```python class Version: def __init__(self, major, minor): self.major = major self.minor = minor def _key(self): return (self.major, self.minor) def __eq__(self, other): if not isinstance(other, Version): return NotImplemented return self._key() == other._key() def __hash__(self): return hash(self._key()) ``` One tuple, used twice, makes the "equal implies equal hash" rule true by construction. Returning `NotImplemented` for foreign types — rather than `False` — leaves the other operand a chance to answer instead of hard-coding an answer on its behalf. **2. Restore identity hashing explicitly** with `__hash__ = object.__hash__` in the class body. This is right only when identity really is the hashing identity you want — for example a class that overrides `__eq__` for a comparison-like purpose but must still behave as a unique key. Be honest about the cost: if `__eq__` is value-based, this reintroduces exactly the contradiction Python was protecting you from, and equal objects will hash differently. **3. Stay unhashable on purpose.** For a mutable value object this is often the *correct* outcome, and it is precisely why `list` and `dict` are unhashable: an object whose compared fields can change has no honest, stable hash to offer. Making that explicit — `__hash__ = None` written in the class body — documents the decision for the next reader. ## Details that separate a middle from a junior answer - Only `__eq__` triggers the rule. Defining `__lt__`, `__gt__` or the other ordering dunders leaves `__hash__` alone. - Inheritance follows the same rule per class body: a subclass that overrides `__eq__` becomes unhashable even though its parent was perfectly hashable, and it must redefine `__hash__` too. - Setting `__hash__` to `None` explicitly is the supported way to *remove* hashability from an otherwise hashable class. - Immutability is not enforced by any of this. Python trusts you: nothing stops you from writing a `__hash__` over fields you later mutate, and nothing will tell you when you do. - A hash may legally collide. `hash` values do not have to be unique; equality does the final discrimination. What they may not do is differ for objects that are equal. The short interview answer is the whole causal chain in one breath: defining `__eq__` invalidates the inherited identity hash, so Python removes it by setting `__hash__` to `None`, so the instance becomes unhashable, so containers reject it — and the fix is to supply a `__hash__` derived from the same fields the new `__eq__` compares.
- Your class must keep identity hashing even though it defines `__eq__` — how do you get it back?Write `__hash__ = object.__hash__` in the class body, which rebinds the inherited identity hash and undoes the automatic `None`. It is legitimate when identity really is the key identity you want, but be explicit about the consequence: if `__eq__` compares field values, two equal objects will now hash differently, so hash-based lookups will miss them. Prefer hashing the same fields `__eq__` compares.
- Is leaving a class unhashable ever the right answer?Yes, and it is why `list`, `dict` and `set` are unhashable. If the fields your `__eq__` compares can be mutated, there is no stable hash to offer, and any container that accepted the object would be corrupted by the first mutation. Failing fast at insertion is far better. Write `__hash__ = None` in the class body to make that decision explicit.
- Does defining `__lt__` or the other ordering dunders also remove hashability?No. Only `__eq__` triggers the rule, because only `__eq__` changes the notion of sameness that hashing must agree with. Ordering has no contract with `__hash__`, so a class that defines only comparison operators keeps the inherited identity hash and stays usable as a key.
saying these in an interview costs you the question
- Says Python raises the error at class definition time
- Claims `__hash__` is inherited unchanged after defining `__eq__`
- Thinks defining any dunder removes hashability
- Suggests hashing fields the `__eq__` does not compare
- Restores `object.__hash__` without noting the broken contract
- Believes hash values must be unique per instance