Why must dictionary keys be hashable?

Because the entry is stored in a slot chosen from the key's hash. If the key could change afterwards, its hash would change, the lookup would go to a different slot, and the entry would be unreachable. So keys must be immutable — tuples and strings qualify, lists and dicts do not.

Overview

The requirement

A dictionary key must be hashable, which means two things: it has a __hash__ method returning a stable integer, and it can be compared for equality.

1Python
Output

The rule follows directly from how a dictionary works. The hash determines which slot an entry occupies. If a key's hash changed after insertion, the entry would sit in a slot the new hash does not point to — unreachable, and effectively lost.

So the requirement is not arbitrary strictness; it is what keeps the structure coherent.

Dicts, sets & hashingConceptualMedium

Step through it

What to watch

  • The rule follows from the storage, not from taste.
  • Mutating a key would leave the entry in a slot nothing looks at.
  • A tuple containing a list is also unhashable — it goes all the way down.

Say this out loud

"The key's hash decides where the entry lives. If the key could mutate, the hash would change and you could never find the entry again - so keys have to be immutable. A tuple works; a list doesn't."

Why must dictionary keys be hashable?

Why can a tuple be a dictionary key but a list cannot?

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.

2Python
Output

Demonstrating the failure

Python prevents this for built-in types, and a custom class can be made to break it:

3Python
Output

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:

4Python
Output

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

TypeHashableNote
int, float, str, bytesYesImmutable
tupleYes, if its contents areA tuple containing a list is not
frozensetYesThe immutable set
None, True, FalseYesSingletons
list, dict, setNoMutable
bytearrayNoMutable
Custom class, defaultYesIdentity-based hash
Custom class with __eq__ onlyNo__hash__ is removed
dataclass(frozen=True)YesGenerates both correctly

The tuple row is the subtle one:

5Python
Output

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.

How the code works

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.

How the code works

  1. hash((1, [2]))Fails, because a tuple's hash is computed from its contents. Immutability has to hold all the way down, not just at the top level.
  2. key.tag = "b"The contract broken deliberately. The entry stays in the slot chosen by the old hash while lookups go to the new one, so it is lost while still occupying memory.
  3. list(d.items())The entry is still there and still iterable — only lookup by key is broken. That is what makes this class of bug so hard to diagnose.
  4. @dataclass(frozen=True)Generates __eq__ and __hash__ over the same fields and blocks assignment. It is the correct way to make a value object usable as a key.

Change one thing

  • Define __eq__ on a class without __hash__ and try to use it as a key. Python sets the hash to None for you — deliberately.
  • Use a plain @dataclass instead of a frozen one. It is unhashable, for exactly the reason this page is about.

Where this runs

Real CPython, compiled to WebAssembly and running on your own machine — nothing is uploaded. The first run takes a few seconds while the interpreter downloads; after that it is immediate. Need more room, or want to paste your own attempt? Use the Python compiler.

Check yourself

0 of 3

Answer without scrolling back up.

  1. A list cannot be a dictionary key because:

  2. Is (1, [2]) hashable?

  3. Defining __eq__ on a class without __hash__ makes instances:

Cheat sheet

Why must dictionary keys be hashable?

Because the entry is stored in a slot chosen from the key's hash. If the key could change afterwards, its hash would change, the lookup would go to a different slot, and the entry would be unreachable. So keys must be immutable — tuples and strings qualify, lists and dicts do not.

INTERVIEW · vizlearn.in/interview/why-must-dict-keys-be-hashable.html

About the author

Ashish Jangra builds and maintains VizLearn. Every module here is written and the visualisation behind it hand-built, so the numbers in a readout come from the same code that draws the picture. Corrections are genuinely welcome and get priority over everything else — if a page states something wrong, or an animation misrepresents what the algorithm does, get in touch.