TypeError: unhashable type: 'list' means you used something changeable where Python needed something fixed — as a dictionary key, or as a member of a set. Dictionaries and sets find items by hash, and a value that can change would break that lookup. The fix is to convert to an immutable equivalent, usually a tuple.
What hashable means#
hash("hello") # fine
hash(42) # fine
hash((1, 2)) # fine
hash([1, 2]) # TypeError: unhashable type: 'list'
A dictionary stores each entry in a slot chosen by the key’s hash. If the key could change afterwards, its hash would change, the entry would be in the wrong slot, and the value would become unreachable. Python prevents that by refusing mutable keys outright.
| Hashable | Not hashable |
|---|---|
str, int, float, bool, None |
list |
tuple of hashables |
dict |
frozenset |
set |
bytes |
bytearray |
| most custom objects | tuple containing a list |
Where it appears#
# 1. As a dictionary key
data = {}
data[["a", "b"]] = 1 # TypeError
# 2. In a set
seen = set()
seen.add([1, 2]) # TypeError
# 3. Deduplicating a list of lists
unique = set([[1, 2], [3, 4]]) # TypeError
# 4. Counting
from collections import Counter
Counter([[1], [2]]) # TypeError
# 5. Grouping by a computed key
groups = {}
for row in rows:
groups[[row["a"], row["b"]]] = row # TypeError
Fix 1: use a tuple#
data = {}
data[("a", "b")] = 1
seen = set()
seen.add((1, 2))
unique = set(tuple(item) for item in [[1, 2], [3, 4], [1, 2]])
print(unique) # {(1, 2), (3, 4)}
For the grouping case:
groups = {}
for row in rows:
key = (row["country"], row["year"])
groups.setdefault(key, []).append(row)
Tuples are the right answer whenever order matters and the number of elements is fixed.
Fix 2: use a frozenset when order does not matter#
a = frozenset(["python", "web"])
b = frozenset(["web", "python"])
print(a == b) # True - order is irrelevant
print({a: 1}[b]) # 1 - the same key
tags = {}
tags[frozenset({"beginner", "python"})] = "tutorial"
Compare with tuples, where ("a", "b") and ("b", "a") are different keys. Choose based on whether the ordering carries meaning.
Fix 3: a canonical string for nested data#
import json
def key_for(obj):
"""A stable string key for arbitrary nested JSON-like data."""
return json.dumps(obj, sort_keys=True, separators=(",", ":"))
cache = {}
cache[key_for({"b": 2, "a": [1, 2]})] = "result"
print(cache[key_for({"a": [1, 2], "b": 2})]) # same key, found
sort_keys=True is what makes it canonical — two dictionaries with the same contents in a different order produce the same string. Without it, this quietly fails to match.
Fix 4: recursive conversion#
def make_hashable(obj):
"""Convert nested structures into hashable equivalents."""
if isinstance(obj, (list, tuple)):
return tuple(make_hashable(x) for x in obj)
if isinstance(obj, dict):
return tuple(sorted((k, make_hashable(v)) for k, v in obj.items()))
if isinstance(obj, (set, frozenset)):
return frozenset(make_hashable(x) for x in obj)
return obj
key = make_hashable({"tags": ["a", "b"], "id": 1})
print(hash(key)) # works
Useful for caching function results keyed on complex arguments.
The tuple-containing-a-list trap#
key = (1, [2, 3])
hash(key) # TypeError: unhashable type: 'list'
A tuple is only hashable if everything inside it is. The error still names list, which is confusing when the thing you passed was a tuple. Convert recursively, as above.
Fix 5: make your own class hashable#
class Point:
def __init__(self, x, y):
self.x = x
self.y = y
def __eq__(self, other):
return isinstance(other, Point) and (self.x, self.y) == (other.x, other.y)
p = Point(1, 2)
{p: "here"} # TypeError: unhashable type: 'Point'
Custom classes are hashable by default — but defining __eq__ removes that, because two objects that compare equal must hash equally, and Python cannot guess how. Supply both:
class Point:
def __init__(self, x, y):
self.x = x
self.y = y
def __eq__(self, other):
return isinstance(other, Point) and (self.x, self.y) == (other.x, other.y)
def __hash__(self):
return hash((self.x, self.y))
A frozen dataclass does both for you:
from dataclasses import dataclass
@dataclass(frozen=True)
class Point:
x: float
y: float
points = {Point(1, 2): "origin-ish"}
print(Point(1, 2) in points) # True
The frozen=True is what makes it immutable and therefore safely hashable. Without it, eq=True alone sets __hash__ to None.
The caching case#
from functools import lru_cache
@lru_cache(maxsize=128)
def summarise(values):
return sum(values) / len(values)
summarise([1, 2, 3]) # TypeError - arguments must be hashable
summarise((1, 2, 3)) # fine
lru_cache keys its cache on the arguments, so they must be hashable. Either pass tuples, or wrap the function:
def summarise(values):
return _summarise(tuple(values))
@lru_cache(maxsize=128)
def _summarise(values):
return sum(values) / len(values)
Questions people ask#
Why can a tuple be a key but not a list?
A tuple cannot be changed after creation, so its hash is stable for its whole lifetime. A list can, so its hash would become wrong.
Are all tuples hashable?
Only if every element is. A tuple containing a list or a dictionary is not.
How do I use a dictionary as a key?
You cannot directly. Convert to a sorted tuple of items, a frozenset of items, or a canonical JSON string. Which is right depends on whether you need to recover the original.
Is a string always hashable?
Yes, and so are numbers, booleans, None, bytes and frozensets. All are immutable.
Where to go next#
- Python dictionaries explained — how keys and hashing work.
- Solving dict TypeError — the wider family of these errors.
- Object-oriented programming in Python — the dunder methods above.