Pythonerrorsdictset

Python TypeError: unhashable type 'list' — How to Fix It

TypeError: unhashable type: 'list' shows up when a list lands in a dict key, a set, or an in test against a set. Here is what hashable really means, why a tuple works where a list fails, and how to fix deduplicating a list of lists, grouping by a composite key, and using a dict as a key.

9 min readReferencePython · errors · dict · set · traceback

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

  1. 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.
  2. 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).
  3. Trusting dict.get(). The default does not save you: prices.get(["Moscow"], 0) fails just like prices[["Moscow"]].
  4. Freezing halfway. ("Moscow", ["bread", "milk"]) is still unhashable — the nested list has to become a tuple too.
  5. 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.
  6. 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.

Practice on real tasks

Solve tasks in the Python trainer with instant grading and hints.

Open trainer