skip to content

Why can a tuple be used as a dict key when a list cannot?

level: middleimportance: should knowfreq 62%

answer

  1. What must stay true of a key?
  2. Dict keys and set members share one requirement
  3. list, dict and set set __hash__ to None
  4. A container hashes its contents too
  5. frozenset is the hashable set

basics

~20 s

Dict keys and set members must be hashable, and Python only makes objects hashable when their value is fixed. list, dict and set set __hash__ to None, so hash([1, 2]) raises TypeError; tuple, frozenset, str and the numeric types hash fine.

solid answer

~50 s

A dict key must be **hashable**: it must have a hash value that never changes for the object's lifetime. Python grants that only to types whose value is fixed at construction, so `tuple`, `frozenset`, `str`, `bytes`, `int`, `float` and `None` are hashable, while `list`, `dict`, `set` and `bytearray` deliberately are not — they set `__hash__` to `None`, which is why `hash([1, 2])` and `{[1, 2]: 'x'}` raise `TypeError`. The reason is practical: an object filed under one hash and then mutated would be filed in the wrong place and effectively unfindable. Hashability is also *recursive* for containers: a tuple hashes its elements, so `hash((1, [2]))` fails even though the tuple itself is immutable. When you need a set-like key, use `frozenset`; when you need a mutable structure as a key, convert it to a canonical immutable form first.

code

python · 7 lines
python
prices = {("bidder-a", 42): 4.75}
print(prices[("bidder-a", 42)])
print(list.__hash__, tuple.__hash__ is not None)
try:
    prices[["bidder-a", 42]] = 4.75
except TypeError as exc:
    print(exc)

go deeper

for a junior

Recall the short version: dict keys and set members must be hashable, and the mutable built-ins are not. Being able to say list fails while tuple and str work is enough here.

for a middle

Explain the requirement as a stable hash for the object's lifetime, name __hash__ = None on the mutable built-ins, and show that a tuple hashes its contents so hash((1, [2])) still fails.

for a senior

Demonstrate turning real mutable data into a canonical key — tuple(sorted(...)) or a frozenset of items — and explain why canonicalisation matters when two structures you treat as equivalent must map to one entry.

for a principal

Argue for immutable value types over ad-hoc composite keys: a frozen dataclass or named tuple carries meaning, hashes on its fields, and keeps a caching or indexing contract stable while the surrounding code evolves.

## The rule A `dict` key and a `set` member must be **hashable**. In Python that means two things at once: `hash(obj)` returns an integer, and that integer does not change while the object lives. Everything else about dict and set behaviour follows from wanting that guarantee. The built-in types divide exactly along the mutability line: - **Hashable**: `int`, `float`, `complex`, `bool`, `str`, `bytes`, `tuple`, `frozenset`, `range`, `None`, functions, classes, modules. - **Not hashable**: `list`, `dict`, `set`, `bytearray`. The unhashable ones are unhashable *on purpose*, not by omission. Each sets its `__hash__` slot to `None`, so the lookup that `hash()` performs finds nothing callable: ```python print(list.__hash__) # None hash([1, 2]) # TypeError: unhashable type: 'list' ``` On 3.14 the dict-key case gives an even more direct message — `cannot use 'list' as a dict key (unhashable type: 'list')` — which is worth recognising because it names both the operation and the cause. ## Why mutability disqualifies a key The dict stores an entry under a slot derived from the key's hash. If a key's contents changed after insertion, its hash would change too, and a later lookup would compute the new hash, land in a different place, and fail to find an entry that is demonstrably still in the dict. Rather than let you build that broken state, Python refuses the key up front. That is a deliberate design choice: some languages allow mutable keys and leave the consequences to you, and Python does not. It is worth being precise that the requirement is *stable hash*, not *immutability* as such — immutability is simply the way the built-ins guarantee stability. Instances of your own classes are hashable by default even though they are mutable, because their default hash comes from identity rather than contents, so it stays stable no matter what you change. Defining equality on such a class changes that picture, and that contract is a topic in its own right. ## Hashability is recursive A tuple does not have a hash of its own independent of what it holds; `tuple.__hash__` combines the hashes of its elements. So a tuple is hashable only if everything inside it is: ```python hash(("campaign-7", 42)) # fine hash(("campaign-7", [42])) # TypeError: unhashable type: 'list' ``` This is the most common surprise in this area, and the honest way to state the rule is: *a tuple is hashable when its contents are*. The same applies to `frozenset`, which is why a `frozenset` can only ever contain hashable members in the first place. ## Getting a key out of mutable data When the natural key is a mutable structure, convert it into a canonical immutable form: - A list of values becomes a `tuple`: `key = tuple(path)`. - A set of labels becomes a `frozenset`, which also makes the key order-insensitive: `frozenset({"ctr", "bid"}) == frozenset({"bid", "ctr"})`, and their hashes match. - A flat mapping becomes `frozenset(mapping.items())` when order is irrelevant, or `tuple(sorted(mapping.items()))` when you want a deterministic ordering — both require the values themselves to be hashable. The word *canonical* is doing real work there. Two structures you consider equivalent must produce the same key, or you will simply store two entries where you expected one. `tuple(sorted(...))` gives that; `tuple(mapping.items())` does not, because insertion order would leak into the key. ## Value objects of your own When the key is a domain concept rather than a raw structure, make an immutable value type instead of stringing values together. A `collections.namedtuple` is hashable when its fields are, and a `dataclasses.dataclass(frozen=True)` rejects attribute assignment and generates a hash from its fields. Both read far better at the call site than a bare tuple, and both keep the stability guarantee that dict keys require. ## The practical takeaways If `TypeError: unhashable type` appears, something mutable reached a dict key, a set member, or an API that hashes its inputs behind the scenes. The fix is never to force a hash onto a mutable object; it is to build an immutable snapshot of the data at the moment you use it as a key.

  • Is a tuple always hashable?
    No. `tuple.__hash__` combines the hashes of its elements, so a tuple is hashable only when everything it holds is. `hash((1, [2]))` raises `TypeError: unhashable type: 'list'` even though the tuple's own slots are fixed. This is the clearest demonstration that immutability in Python is shallow.
  • Instances of my own class are mutable, so why can I still use them as dict keys?
    Because their default hash derives from identity, not contents. Mutating attributes never changes it, so the key stays findable — the stability requirement is satisfied by a different route than immutability. Two objects you consider equal will still be separate keys unless you define equality and hashing yourself.
  • How would you key a cache on a small flat mapping of settings?
    Convert it to a canonical immutable form: `frozenset(mapping.items())` when order does not matter, or `tuple(sorted(mapping.items()))` when you want a deterministic key. Both need the values to be hashable too. Canonicalising matters — two mappings you treat as equivalent must produce one key, not two.

saying these in an interview costs you the question

  • Says lists cannot be keys because they have no fixed length
  • Claims a tuple is always hashable whatever it holds
  • Thinks any object can be a key if you give it a hash
  • Believes mutable objects of your own classes cannot be keys
  • Uses tuple(mapping.items()) and ignores ordering
  • Confuses hashable with unique or with sortable

context