Why does hash() raise TypeError on a Python tuple that contains a list?
answer
- Hashability is inherited from the contents
- One unhashable item poisons the whole tuple
- The list type opts out of hashing
- Fails at hash time, not at construction
- Freeze inner lists into tuples
basics
~20 sA tuple's hash is computed by hashing each of its items and mixing the results, so a tuple is hashable only if every item is. Lists opt out of hashing, so the whole tuple becomes unhashable and cannot key a dict.
solid answer
~50 sTuple implements hashing by walking its items, hashing each one and mixing them, so **hashability is inherited from the contents**. A list opts out: its type sets `__hash__` to `None`, because a hash-based container files a key once by its hash and a key whose value could change would become unfindable. Hashing `("invoice", [1, 2])` therefore raises `TypeError: unhashable type: 'list'`, and so does using it as a dict key, adding it to a set, or testing set membership with it - though `in` on a list still works, because that uses `==` and never hashes. Nothing is checked at construction: the error surfaces wherever the tuple is first hashed. The fix is to freeze the contents - nested `tuple` for ordered fields, `frozenset` for unordered ones - accepting that the key is then a snapshot.
code
python · 8 linesgood = ("invoice", (1, 2))
print(isinstance(hash(good), int)) # True
bad = ("invoice", [1, 2]) # constructing it is fine
try:
hash(bad)
except TypeError as exc:
print("unhashable:", exc) # unhashable type: 'list'go deeper
Recall that a list cannot be a dict key and that the ban spreads to any tuple holding one. Recognising TypeError: unhashable type: 'list' and knowing to look at what is inside the tuple is enough at this level.
Explain the mechanism: tuple hashing walks and mixes item hashes, so one unhashable item makes the whole tuple unhashable, and the check happens at hash time rather than at construction. Show the fix by freezing inner collections.
Diagnose it in a real codebase: the traceback lands at the dict or set that hashed the value, not where the shape was chosen, so trace back to the constructor. Decide whether to freeze at the boundary or to key on a derived stable identifier instead.
Own the data-modelling stance. Whether cache and index keys are frozen value objects or derived scalar identifiers determines snapshot semantics, memory, and how many of these errors your team meets at runtime rather than at review time.
### Hashing is a property of a value, and a tuple's value is its items `hash()` asks an object for an integer that stands in for its value, so that hash-based containers can find it in a bucket. Tuple implements this by walking its items, hashing each one, and mixing the results together into a single integer. That design has an immediate consequence: **a tuple is hashable only if every item it holds is hashable.** Hashability is inherited from the contents, exactly as immutability is - and for the same underlying reason. A list is deliberately not hashable. Its type sets `__hash__` to `None`, which is the documented way for a type to opt out. When `hash()` finds `None` there instead of a callable, it raises `TypeError: unhashable type: 'list'`. The rationale is that a hash-based container files a key by its hash once, at insertion time; if the key could then change value, it would sit in the wrong bucket forever and become unfindable even by itself. Excluding mutable containers up front is the cheap way to prevent that whole class of bug. Put the two facts together and the tuple's failure follows mechanically: ```python t = ("invoice", [1, 2]) hash(t) # TypeError: unhashable type: 'list' {t: "value"} # same TypeError - dict hashes the key {t} # same TypeError - set hashes the member t in {("a",)} # same TypeError - membership in a set hashes the left operand t in [("a",)] # fine - list membership uses == and never hashes ``` ### The timing is what catches people out Nothing is checked when the tuple is built. Constructing `("invoice", [1, 2])` is perfectly legal and costs a pointer copy. The `TypeError` arrives only at the moment something hashes the tuple, which in real code is often far away from where it was created - the first time it is used as a dict key, added to a set, deduplicated, or passed to something that caches on its arguments. The traceback points at the hashing site, not at the construction site that actually chose the bad shape, so read the *value* in the message (`'list'`) and work backwards to which field carries it. A second timing detail worth knowing: CPython does not cache a tuple's hash the way it caches a string's. Every `hash()` call on a deeply nested tuple re-walks and re-mixes the whole structure, so a huge nested tuple used as a hot dict key is O(total items) per lookup, not O(1). ### Fixing it The usual fix is to freeze the contents rather than to fight the hash: * an ordered sequence field becomes a nested `tuple`; * an unordered collection field becomes a `frozenset` (which also makes two orderings compare and hash the same, often what you actually wanted); * a mapping field becomes a tuple of sorted key/value pairs. ```python record = ("invoice", [1, 2]) key = tuple(tuple(v) if isinstance(v, list) else v for v in record) counts = {key: 1} ``` Remember what freezing buys you: the key is a *snapshot*. Later changes to the original list do not reach the key, which is the point - but it also means the key and the live object can drift, and code that expects to look the record up again by its current contents will miss. ### Two adjacent facts that show up in follow-ups **Hashable is not the same as immutable.** An ordinary user-defined class is hashable by default - its hash comes from identity - even though its attributes are freely mutable. Immutability is the common *reason* something is hashable, not the definition. **The rule is recursive, not one-level.** `(1, (2, [3]))` is unhashable too; the list is two levels down, and the mixing walk reaches it. Conversely `(1, frozenset({2, 3}))` hashes fine, while `(1, {2, 3})` - an ordinary set - does not, and `(1, {"a": 2})` does not either, because dicts are unhashable as well. ### The one-line interview answer Tuple hashing recurses into the items, a list refuses to be hashed because its value can change, so any tuple containing a list is unhashable and cannot key a dict - and the failure surfaces at hash time, not at construction.
- Is the tuple rejected when it is built, or later?Later. Building `("invoice", [1, 2])` is legal and cheap - it copies references. The `TypeError` appears only when something actually hashes the tuple: a dict insertion or lookup, a set add, a deduplication pass. That is why the traceback usually points far away from the code that chose the bad shape, and you have to work backwards from the type named in the message.
- Does the same tuple work with the `in` operator?It depends on the right-hand container. `t in some_list` works, because list membership compares with `==` and never hashes the left operand. `t in some_set` and `t in some_dict` raise the same `TypeError`, because both hash the operand to pick a bucket. Two containers, same expression, different outcome - a classic source of an error that only appears after someone swaps a list for a set.
- Does CPython cache a tuple's hash the way it caches a string's?No. `str` stores its hash after the first computation; tuple recomputes on every call, walking and mixing all its items again. For a small record that is irrelevant, but a large nested tuple used as a hot dict key costs time proportional to its total element count on every lookup, not constant time. If that shows up in a profile, key on a precomputed scalar instead.
saying these in an interview costs you the question
- Says the tuple is rejected when it is constructed
- Claims tuples are always hashable because they are immutable
- Thinks hashable and immutable mean the same thing
- Believes a tuple containing a set is hashable
- Assumes the error is about the tuple rather than the list
- Says converting the outer tuple to a frozenset fixes it