Python TypeError unhashable type list means one thing: you put a list where Python requires a hashable object — as a dictionary key or as a set member. The fix is mechanical: use tuple(row) instead of row, because a tuple can be hashed while a list cannot.
The full message is TypeError: unhashable type: 'list'. The type at the end changes — 'dict' and 'set' appear too — but the cause is always the same, and every place beginners hit it is covered below.
What does TypeError: unhashable type: 'list' mean?
The message means Python tried to compute a hash of a list and refused. Dictionaries and sets need that hash: it is how they jump straight to the right slot instead of scanning everything. A list does not support hashing, so any attempt to use it as a dict key, a set member, or the left operand of in against a set fails with this error.
A minimal example, file prices.py:
prices = {}
key = ["Moscow", "bread"]
prices[key] = 45
Traceback (most recent call last):
File "prices.py", line 3, in <module>
prices[key] = 45
~~~~~~^^^^^
TypeError: unhashable type: 'list'
The ~~~~~~^^^^^ markers (Python 3.11 and newer draws them for you; the exact underlining shifts a little between versions) point at prices[key]: the list itself is not the problem, using it as a key is. The working version differs by one word:
prices = {}
key = ["Moscow", "bread"]
prices[tuple(key)] = 45
print(prices)
{('Moscow', 'bread'): 45}
What is a hashable object in plain words?
A hashable object is one you can call hash() on, where the resulting number never changes while the object is alive. A dictionary files keys into buckets by that number and looks them up by the same number. If an object changed after it was filed, the bucket would no longer match and the key would be lost. That is why immutable objects — numbers, strings, tuples, frozenset, None, True — are hashable, while mutable ones — list, dict, set — are not.
print(hash(42))
print(hash((1, 2)) == hash((1, 2)))
print(list.__hash__ is None)
print(tuple.__hash__ is None)
42
True
True
False
list.__hash__ is None is not an oversight — hashing is disabled for list on purpose. A class that hashes a mutable attribute shows what the ban prevents:
class City:
def __init__(self, name):
self.name = name
def __hash__(self):
return hash(self.name)
def __eq__(self, other):
return self.name == other.name
msk = City("Moscow")
seen = {msk}
msk.name = "Kazan"
print(msk in seen)
False
The object is inside the set, and the set can no longer find it: the hash moved, the bucket did not. Lists are protected from exactly this class of bug. For the correct way to pair __eq__ and __hash__ in your own classes, see equality and hashing in the handbook.
Why can a tuple be a dictionary key when a list cannot?
A tuple works because it cannot be changed: once ("Moscow", "bread") exists you cannot replace, append or delete its items, so its hash is computed once and stays valid forever. A list is rewritten by append, remove, sort and index assignment, so its hash would have to be recomputed — and the dictionary would never hear about it. Hence the rule of thumb: a composite key is always a tuple.
| Object |
Hashable |
Use instead |
[1, 2] — list |
no |
(1, 2) — tuple |
{"city": "Moscow"} — dict |
no |
tuple of values or a JSON string |
{1, 2} — set |
no |
frozenset({1, 2}) |
("Moscow", [1, 2]) — tuple holding a list |
no |
("Moscow", (1, 2)) |
(1, 2) — tuple of immutables |
yes |
— |
"bread", 45, 3.5, True, None |
yes |
— |
The tuple as an immutable sequence is covered separately: tuple in the handbook.
Where the error shows up most often
Three scenarios account for almost every case: a list used as a dict key, deduplication through set(), and a membership test against a set. The last two rows of the table are sibling messages with the same root cause — dicts and sets are mutable too, so they cannot be keys either.
| What you do |
Line of code |
Message |
| list as a dictionary key |
prices[["Moscow", "bread"]] = 45 |
unhashable type: 'list' |
| deduplicating a list of lists |
set(rows) |
unhashable type: 'list' |
| looking a list up in a set |
row in seen |
unhashable type: 'list' |
| dict as a dictionary key |
counts[{"city": "Moscow"}] = 5 |
unhashable type: 'dict' |
| set inside a set |
groups.add({"bread", "milk"}) |
unhashable type: 'set' |
One surprise: dict.get() looks like a safe read with a default, and it fails the same way (file prices.py):
prices = {}
prices.get(["Moscow", "bread"], 0)
Traceback (most recent call last):
File "prices.py", line 2, in <module>
prices.get(["Moscow", "bread"], 0)
TypeError: unhashable type: 'list'
The default value protects you from a missing key, not from an unhashable one: to decide whether the key is there at all, the dictionary needs a hash first.
How do you remove duplicates from a list of lists?
Convert every inner row to a tuple and deduplication starts working. A plain set(rows) over a list of lists always fails, because a set must hash every element you put into it (file dedup.py):
rows = [["bread", 45], ["milk", 89], ["bread", 45]]
unique = set(rows)
Traceback (most recent call last):
File "dedup.py", line 2, in <module>
unique = set(rows)
^^^^^^^^^
TypeError: unhashable type: 'list'
If order does not matter, one comprehension is enough:
rows = [["bread", 45], ["milk", 89], ["bread", 45]]
unique = {tuple(row) for row in rows}
print(sorted(unique))
[('bread', 45), ('milk', 89)]
If order matters and you need lists back, use the "seen set" pattern:
rows = [["bread", 45], ["milk", 89], ["bread", 45]]
seen = set()
unique = []
for row in rows:
key = tuple(row)
if key not in seen:
seen.add(key)
unique.append(row)
print(unique)
[['bread', 45], ['milk', 89]]
Shorter variants: dict.fromkeys keeps the order of first appearance, Counter also gives counts:
from collections import Counter
rows = [["bread", 45], ["milk", 89], ["bread", 45]]
print(list(dict.fromkeys(map(tuple, rows))))
print(Counter(tuple(row) for row in rows))
[('bread', 45), ('milk', 89)]
Counter({('bread', 45): 2, ('milk', 89): 1})
What you should not do is delete duplicates while walking the list — that has a trap of its own, where adjacent duplicates survive every other time, explained in deleting from a list while looping.
Grouping by a composite key: tuple instead of list
The second most common trigger is a report where rows are summed by a pair of columns. Make the key a tuple of those fields and no list reaches the key:
orders = [
("Moscow", "bread", 45),
("Moscow", "milk", 89),
("Kazan", "bread", 42),
("Moscow", "bread", 47),
]
totals = {}
for city, product, price in orders:
key = (city, product)
totals[key] = totals.get(key, 0) + price
print(totals)
{('Moscow', 'bread'): 92, ('Moscow', 'milk'): 89, ('Kazan', 'bread'): 42}
When rows arrive as lists — from a CSV, for example — slice and convert: key = tuple(row[:2]). To collect values per group instead of summing them, reach for defaultdict:
from collections import defaultdict
rows = [
["Moscow", "bread", 45],
["Kazan", "bread", 42],
["Moscow", "bread", 47],
]
groups = defaultdict(list)
for city, product, price in rows:
groups[(city, product)].append(price)
print(dict(groups))
{('Moscow', 'bread'): [45, 47], ('Kazan', 'bread'): [42]}
Note that the values stay ordinary lists, and that is perfectly legal: hashability is required of dictionary keys and set members only. Drill this kind of grouping on Merge similar items.
frozenset: when the key is a set
frozenset is an immutable set, and it hashes. Use it when the key is conceptually an unordered collection: a tag set, a team roster, a combination of options. Two frozensets with the same contents are equal and hash the same, no matter what order the elements were written in.
tags = {}
tags[frozenset({"python", "sql"})] = 12
tags[frozenset({"python", "docker"})] = 3
print(tags[frozenset({"sql", "python"})])
12
A tuple cannot do that: ("python", "sql") and ("sql", "python") are two different keys. Pick a tuple when field order carries meaning (city, product), frozenset when only membership does — see frozenset as a dictionary key.
Why is a tuple with a list inside also unhashable?
Because a tuple's immutability is a promise about references, not about the objects behind them. The tuple guarantees its slots keep pointing at the same objects, but if one of those objects is a list, its contents can still change. A tuple's hash is built from the hashes of its elements, so the computation dies on the list (file keys.py):
key = ("Moscow", ["bread", "milk"])
print(hash(key))
Traceback (most recent call last):
File "keys.py", line 2, in <module>
print(hash(key))
^^^^^^^^^
TypeError: unhashable type: 'list'
The fix is to freeze all the way down: ("Moscow", ("bread", "milk")). For arbitrary nesting, serialize the structure into a string — see the next section.
What if the key has to stay mutable?
The list itself can stay mutable — what goes into the key is a snapshot of it. tuple(row) copies the current contents, the list then lives its own life, and the key stays exactly what it was at insertion time. That is not a side effect; it is the behaviour that makes dictionaries predictable.
key = ["Moscow", "bread"]
prices = {tuple(key): 45}
key.append("2026")
print(prices)
print(("Moscow", "bread") in prices)
{('Moscow', 'bread'): 45}
True
The logic matches another common confusion — changing one object does not rewrite another one for you — covered in the article on why assigning to the loop variable does not change the list.
When the key really is a dictionary, you have three options:
| Situation |
Solution |
Why it works |
| fixed set of fields |
tuple(d[k] for k in ("city", "product")) |
fast and readable key |
| unknown set of fields |
json.dumps(d, sort_keys=True) |
sort_keys cancels the effect of key order |
| key is really a set of pairs |
frozenset(d.items()) |
fine as long as every value is hashable |
import json
records = [
{"city": "Moscow", "product": "bread"},
{"product": "bread", "city": "Moscow"},
{"city": "Kazan", "product": "bread"},
]
seen = set()
unique = []
for row in records:
key = json.dumps(row, sort_keys=True)
if key not in seen:
seen.add(key)
unique.append(row)
print(len(unique))
2
The first two dictionaries differ only in key order, and sort_keys=True collapses them into one key. That is why str(row) is a poor substitute here: it does not normalise order. One caveat when the fields hold non-ASCII text: json.dumps escapes it by default, so the key becomes a long run of escape sequences — harmless for lookups, but the same ensure_ascii switch is what makes such files unreadable on disk, explained in Cyrillic saved as Unicode escapes.
How is this different from other list TypeErrors?
unhashable type: 'list' fires when a list is used as a dict key or a set member, while list indices must be integers or slices, not str fires the other way round — when a list is indexed like a dictionary, with a string in brackets. The first says "a list cannot go into a key"; the second says "a list cannot be indexed by a key". The second one, with its typical JSON payload scenario, is covered in list indices must be integers.
Keep the neighbours straight: KeyError — the key is hashable but absent; IndexError — the index is out of range; unhashable type — the lookup never started.
Common mistakes
- Converting on write but not on read.
seen.add(tuple(row)) followed by if row in seen still crashes. Convert on both sides: if tuple(row) in seen.
- Using
str(row) instead of tuple(row). It survives for lists, but str({"city": "Moscow", "product": "bread"}) and str({"product": "bread", "city": "Moscow"}) differ for equal dicts. Use json.dumps(..., sort_keys=True).
- Trusting
dict.get(). The default does not save you: prices.get(["Moscow"], 0) fails just like prices[["Moscow"]].
- Freezing halfway.
("Moscow", ["bread", "milk"]) is still unhashable — the nested list has to become a tuple too.
- Making values immutable too. The restriction applies to keys and set members only; a value can be a list, as in the
defaultdict(list) example.
- Faking a set key with a tuple. If element order is meaningless,
tuple(sorted(...)) works, but frozenset is cleaner.
Practice in the browser
The quickest way to internalise keys and sets is to write them, with nothing to install. Two tasks lean entirely on hashable keys: Merge similar items — group by key and sum values — and Isogram — a set of characters already seen. Then take the free dictionary block.
In short
TypeError: unhashable type: 'list' means a list reached a dict key, a set, or an in test against a set.
- For built-in types hashable equals immutable: numbers, strings, tuples and
frozenset qualify; list, dict and set do not.
- The universal fix for a composite key is
tuple(row) — applied both when writing and when looking up.
- Deduplicating a list of lists:
{tuple(row) for row in rows}, or the seen pattern when order matters.
- An unordered key is a
frozenset; a dictionary key is json.dumps(..., sort_keys=True).
- A tuple holding a list is unhashable too: tuple immutability does not extend to its contents.
- Dictionary values may be any mutable objects — the restriction is on keys only.
Python TypeError unhashable type list means one thing: you put a list where Python requires a hashable object — as a dictionary key or as a set member. The fix is mechanical: use
tuple(row)instead ofrow, because a tuple can be hashed while a list cannot.The full message is
TypeError: unhashable type: 'list'. The type at the end changes —'dict'and'set'appear too — but the cause is always the same, and every place beginners hit it is covered below.What does TypeError: unhashable type: 'list' mean?
The message means Python tried to compute a hash of a list and refused. Dictionaries and sets need that hash: it is how they jump straight to the right slot instead of scanning everything. A list does not support hashing, so any attempt to use it as a dict key, a set member, or the left operand of
inagainst a set fails with this error.A minimal example, file
prices.py:prices = {} key = ["Moscow", "bread"] prices[key] = 45The
~~~~~~^^^^^markers (Python 3.11 and newer draws them for you; the exact underlining shifts a little between versions) point atprices[key]: the list itself is not the problem, using it as a key is. The working version differs by one word:prices = {} key = ["Moscow", "bread"] prices[tuple(key)] = 45 print(prices)What is a hashable object in plain words?
A hashable object is one you can call
hash()on, where the resulting number never changes while the object is alive. A dictionary files keys into buckets by that number and looks them up by the same number. If an object changed after it was filed, the bucket would no longer match and the key would be lost. That is why immutable objects — numbers, strings, tuples,frozenset,None,True— are hashable, while mutable ones — list, dict, set — are not.print(hash(42)) print(hash((1, 2)) == hash((1, 2))) print(list.__hash__ is None) print(tuple.__hash__ is None)list.__hash__ is Noneis not an oversight — hashing is disabled forliston purpose. A class that hashes a mutable attribute shows what the ban prevents:class City: def __init__(self, name): self.name = name def __hash__(self): return hash(self.name) def __eq__(self, other): return self.name == other.name msk = City("Moscow") seen = {msk} msk.name = "Kazan" print(msk in seen)The object is inside the set, and the set can no longer find it: the hash moved, the bucket did not. Lists are protected from exactly this class of bug. For the correct way to pair
__eq__and__hash__in your own classes, see equality and hashing in the handbook.Why can a tuple be a dictionary key when a list cannot?
A tuple works because it cannot be changed: once
("Moscow", "bread")exists you cannot replace, append or delete its items, so its hash is computed once and stays valid forever. A list is rewritten byappend,remove,sortand index assignment, so its hash would have to be recomputed — and the dictionary would never hear about it. Hence the rule of thumb: a composite key is always a tuple.[1, 2]— list(1, 2)— tuple{"city": "Moscow"}— dict{1, 2}— setfrozenset({1, 2})("Moscow", [1, 2])— tuple holding a list("Moscow", (1, 2))(1, 2)— tuple of immutables"bread",45,3.5,True,NoneThe tuple as an immutable sequence is covered separately: tuple in the handbook.
Where the error shows up most often
Three scenarios account for almost every case: a list used as a dict key, deduplication through
set(), and a membership test against a set. The last two rows of the table are sibling messages with the same root cause — dicts and sets are mutable too, so they cannot be keys either.prices[["Moscow", "bread"]] = 45unhashable type: 'list'set(rows)unhashable type: 'list'row in seenunhashable type: 'list'counts[{"city": "Moscow"}] = 5unhashable type: 'dict'groups.add({"bread", "milk"})unhashable type: 'set'One surprise:
dict.get()looks like a safe read with a default, and it fails the same way (fileprices.py):prices = {} prices.get(["Moscow", "bread"], 0)The default value protects you from a missing key, not from an unhashable one: to decide whether the key is there at all, the dictionary needs a hash first.
How do you remove duplicates from a list of lists?
Convert every inner row to a tuple and deduplication starts working. A plain
set(rows)over a list of lists always fails, because a set must hash every element you put into it (filededup.py):rows = [["bread", 45], ["milk", 89], ["bread", 45]] unique = set(rows)If order does not matter, one comprehension is enough:
rows = [["bread", 45], ["milk", 89], ["bread", 45]] unique = {tuple(row) for row in rows} print(sorted(unique))If order matters and you need lists back, use the "seen set" pattern:
rows = [["bread", 45], ["milk", 89], ["bread", 45]] seen = set() unique = [] for row in rows: key = tuple(row) if key not in seen: seen.add(key) unique.append(row) print(unique)Shorter variants:
dict.fromkeyskeeps the order of first appearance,Counteralso gives counts:from collections import Counter rows = [["bread", 45], ["milk", 89], ["bread", 45]] print(list(dict.fromkeys(map(tuple, rows)))) print(Counter(tuple(row) for row in rows))What you should not do is delete duplicates while walking the list — that has a trap of its own, where adjacent duplicates survive every other time, explained in deleting from a list while looping.
Grouping by a composite key: tuple instead of list
The second most common trigger is a report where rows are summed by a pair of columns. Make the key a tuple of those fields and no list reaches the key:
orders = [ ("Moscow", "bread", 45), ("Moscow", "milk", 89), ("Kazan", "bread", 42), ("Moscow", "bread", 47), ] totals = {} for city, product, price in orders: key = (city, product) totals[key] = totals.get(key, 0) + price print(totals)When rows arrive as lists — from a CSV, for example — slice and convert:
key = tuple(row[:2]). To collect values per group instead of summing them, reach fordefaultdict:from collections import defaultdict rows = [ ["Moscow", "bread", 45], ["Kazan", "bread", 42], ["Moscow", "bread", 47], ] groups = defaultdict(list) for city, product, price in rows: groups[(city, product)].append(price) print(dict(groups))Note that the values stay ordinary lists, and that is perfectly legal: hashability is required of dictionary keys and set members only. Drill this kind of grouping on Merge similar items.
frozenset: when the key is a set
frozensetis an immutable set, and it hashes. Use it when the key is conceptually an unordered collection: a tag set, a team roster, a combination of options. Two frozensets with the same contents are equal and hash the same, no matter what order the elements were written in.tags = {} tags[frozenset({"python", "sql"})] = 12 tags[frozenset({"python", "docker"})] = 3 print(tags[frozenset({"sql", "python"})])A tuple cannot do that:
("python", "sql")and("sql", "python")are two different keys. Pick a tuple when field order carries meaning (city, product),frozensetwhen only membership does — see frozenset as a dictionary key.Why is a tuple with a list inside also unhashable?
Because a tuple's immutability is a promise about references, not about the objects behind them. The tuple guarantees its slots keep pointing at the same objects, but if one of those objects is a list, its contents can still change. A tuple's hash is built from the hashes of its elements, so the computation dies on the list (file
keys.py):key = ("Moscow", ["bread", "milk"]) print(hash(key))The fix is to freeze all the way down:
("Moscow", ("bread", "milk")). For arbitrary nesting, serialize the structure into a string — see the next section.What if the key has to stay mutable?
The list itself can stay mutable — what goes into the key is a snapshot of it.
tuple(row)copies the current contents, the list then lives its own life, and the key stays exactly what it was at insertion time. That is not a side effect; it is the behaviour that makes dictionaries predictable.key = ["Moscow", "bread"] prices = {tuple(key): 45} key.append("2026") print(prices) print(("Moscow", "bread") in prices)The logic matches another common confusion — changing one object does not rewrite another one for you — covered in the article on why assigning to the loop variable does not change the list.
When the key really is a dictionary, you have three options:
tuple(d[k] for k in ("city", "product"))json.dumps(d, sort_keys=True)sort_keyscancels the effect of key orderfrozenset(d.items())import json records = [ {"city": "Moscow", "product": "bread"}, {"product": "bread", "city": "Moscow"}, {"city": "Kazan", "product": "bread"}, ] seen = set() unique = [] for row in records: key = json.dumps(row, sort_keys=True) if key not in seen: seen.add(key) unique.append(row) print(len(unique))The first two dictionaries differ only in key order, and
sort_keys=Truecollapses them into one key. That is whystr(row)is a poor substitute here: it does not normalise order. One caveat when the fields hold non-ASCII text:json.dumpsescapes it by default, so the key becomes a long run of escape sequences — harmless for lookups, but the sameensure_asciiswitch is what makes such files unreadable on disk, explained in Cyrillic saved as Unicode escapes.How is this different from other list TypeErrors?
unhashable type: 'list'fires when a list is used as a dict key or a set member, whilelist indices must be integers or slices, not strfires the other way round — when a list is indexed like a dictionary, with a string in brackets. The first says "a list cannot go into a key"; the second says "a list cannot be indexed by a key". The second one, with its typical JSON payload scenario, is covered in list indices must be integers.Keep the neighbours straight:
KeyError— the key is hashable but absent;IndexError— the index is out of range;unhashable type— the lookup never started.Common mistakes
seen.add(tuple(row))followed byif row in seenstill crashes. Convert on both sides:if tuple(row) in seen.str(row)instead oftuple(row). It survives for lists, butstr({"city": "Moscow", "product": "bread"})andstr({"product": "bread", "city": "Moscow"})differ for equal dicts. Usejson.dumps(..., sort_keys=True).dict.get(). The default does not save you:prices.get(["Moscow"], 0)fails just likeprices[["Moscow"]].("Moscow", ["bread", "milk"])is still unhashable — the nested list has to become a tuple too.defaultdict(list)example.tuple(sorted(...))works, butfrozensetis cleaner.Practice in the browser
The quickest way to internalise keys and sets is to write them, with nothing to install. Two tasks lean entirely on hashable keys: Merge similar items — group by key and sum values — and Isogram — a set of characters already seen. Then take the free dictionary block.
In short
TypeError: unhashable type: 'list'means a list reached a dict key, a set, or anintest against a set.frozensetqualify; list, dict and set do not.tuple(row)— applied both when writing and when looking up.{tuple(row) for row in rows}, or theseenpattern when order matters.frozenset; a dictionary key isjson.dumps(..., sort_keys=True).