Run it
What is and is not hashable, then a class that breaks the contract on purpose — mutating a key after insertion and losing the entry, exactly as the visualisation describes.
Demonstrating the failure
Python prevents this for built-in types, and a custom class can be made to break it:
The entry exists, the key object is the same object, and it cannot be found. The lookup computes the new hash, goes to a different slot, finds nothing, and raises.
That is the concrete answer to "why?" — and being able to construct the failure demonstrates real understanding rather than recall.
The hash-equality contract
Two rules govern the relationship, and violating the first is the more serious error.
If two objects are equal, their hashes must be equal. Otherwise two keys that compare equal land in different slots, both exist in the dictionary, and d[k] returns whichever was found first.
If two objects have the same hash, they need not be equal. That is an ordinary collision, resolved by comparing the keys and probing.
Consequences for custom classes:
Hash exactly the fields that __eq__ compares. Hashing more fields breaks the first rule; hashing fewer merely causes extra collisions, which is safe but slower.
Defining __eq__ without __hash__ makes a class unhashable in Python 3. That is deliberate: the default __hash__ is identity-based, which would contradict a value-based __eq__. Python removes it rather than let you violate the contract silently.
The invariant being protected
Equal objects must have equal hashes, and an object's hash must not change while it is in use as a key. Both follow from how the table works: the hash is the address, so a moving hash is a moving address.
Python enforces this by making mutable built-ins unhashable. list, dict and set all set __hash__ = None, which is why the error is unhashable type rather than something about dictionaries.
It goes all the way down
A tuple is hashable only if everything in it is. (1, 2) is a fine key; (1, [2]) is not, because the tuple's hash is derived from its contents and one of them can change.
frozenset is the immutable set, and it is hashable for the same reason. set is not, which is why you cannot have a set of sets without it.
Custom objects
By default a custom object hashes by identity, so two equal-looking instances are different keys. Define __eq__ and Python sets __hash__ to None — because you have redefined equality and the default hash no longer agrees with it.
To make it usable as a key, define __hash__ too, over the same fields __eq__ uses, and do not mutate those fields afterwards. @dataclass(frozen=True) does all of this correctly for you.
What is hashable, and what is not
| Type | Hashable | Note |
|---|
int, float, str, bytes | Yes | Immutable |
tuple | Yes, if its contents are | A tuple containing a list is not |
frozenset | Yes | The immutable set |
None, True, False | Yes | Singletons |
list, dict, set | No | Mutable |
bytearray | No | Mutable |
| Custom class, default | Yes | Identity-based hash |
Custom class with __eq__ only | No | __hash__ is removed |
dataclass(frozen=True) | Yes | Generates both correctly |
The tuple row is the subtle one:
A tuple's hash is derived from its contents, so a tuple containing something mutable cannot have a stable hash.
The dataclass row is the practical shortcut: @dataclass(frozen=True) generates __eq__ and __hash__ consistently and makes the instance immutable, which is exactly what a value object used as a key should be.
The workarounds when a key is mutable
Convert to a tuple. tuple(my_list) is the usual answer, and it snapshots the values at that moment — later changes to the list do not affect the key, which is what you want.
Use a frozenset when order should not matter: frozenset(["a", "b"]) equals frozenset(["b", "a"]).
Serialise to a string — json.dumps(obj, sort_keys=True) — for nested structures. Works, and it is slow and fragile about key ordering and types.
Use a canonical form. For dictionaries, tuple(sorted(d.items())) is the common conversion, assuming the values are themselves hashable.
Use the object's id if identity rather than value is the right notion of "same". That is what the default hash does.
The general principle worth stating: if you need a mutable object as a key, what you actually need is an immutable snapshot of the parts that identify it.
Why this appears in interviews
The question is a proxy for whether you understand hash tables rather than just use them. It tests four things:
Do you know the mechanism? Hash determines slot, so a changing hash orphans the entry.
Do you know the contract? Equal objects must hash equally.
Do you know Python's safeguard? Defining __eq__ alone removes __hash__.
Can you produce the failure? Constructing the mutable-key bug is the difference between explanation and recitation.
It also connects to the practical questions asked alongside it — why in is slow on a list, how a dictionary works internally, and why strings are immutable. All four are the same underlying topic approached from different directions.
Questions people ask
Why is a tuple hashable but a list not? Tuples are immutable, so their hash is stable. Lists are not.
Can I hash a nested structure? Only if every part is hashable. A tuple containing a list is not.
What if I define __hash__ without __eq__? Legal — equality falls back to identity, so two equal-valued objects are different keys. Usually not what you want.
Are custom objects hashable by default? Yes, by identity — two instances with the same field values are different keys.
How do I make a class usable as a key? @dataclass(frozen=True), or define __eq__ and __hash__ over the same fields.
Does a bad __hash__ break correctness or just speed? An inconsistent hash breaks correctness. A merely poor one that collides often only costs speed.
Recap in one screen
- The hash determines the slot, so a key whose hash could change would orphan its entry.
- Hashable means a stable
__hash__ plus equality comparison — which in practice means immutable. - Equal objects must have equal hashes; equal hashes need not mean equal objects.
- Defining
__eq__ without __hash__ makes a class unhashable, deliberately. - Convert mutable keys to tuples or frozensets — an immutable snapshot of what identifies the object.