Fix: Python TypeError: unhashable type: 'list'
Part of: Python Errors
Quick Answer
Learn why Python raises TypeError unhashable type list, dict, or set and how to fix it when using dictionary keys, sets, groupby, dataclasses, and custom classes.
The Hash Wall Just Hit You
Personally, this is one of the Python errors I most appreciate. It is loud, it names the type, and it almost always points at a real design issue rather than a typo. I have caught more “I used the wrong data shape” mistakes from this one error than from any amount of static checking. You run your Python script and hit this traceback:
TypeError: unhashable type: 'list'The full traceback usually looks like this:
Traceback (most recent call last):
File "app.py", line 5, in <module>
my_dict = {[1, 2, 3]: "value"}
TypeError: unhashable type: 'list'You might also see the same error with different types:
TypeError: unhashable type: 'dict'
TypeError: unhashable type: 'set'This error means you tried to use a mutable object (a list, dict, or set) in a place that requires a hashable object. Dictionary keys, set members, and anything used as a lookup key in Python must be hashable. Lists, dictionaries, and sets are mutable, so Python cannot compute a stable hash for them, and it raises this error.
The fix depends on what you are trying to do and which data structure triggered the error. Below are the most common scenarios and their solutions.
Quick Reference Before You Dive In
If you arrived here from Google with a fresh traceback, the five facts that resolve roughly 90 percent of cases:
- Hashable means “immutable with a stable
__hash__.” Lists, dicts, and sets are mutable, so Python refuses to compute a hash for them. The Pythonhashableglossary entry and the__hash__data model docs are the canonical sources. tuple(my_list)is the workhorse fix. Most cases resolve by converting the list to a tuple at the hash boundary. For nested structures, recursively freeze withdeep_freeze(Fix 1).- Use
frozensetwhen order does not matter. If you need a set as a dict key,frozenset(my_set)is the immutable equivalent. - Dataclasses need
@dataclass(frozen=True), notunsafe_hash=True. The latter silently breaks dicts and sets the moment any field changes; the former enforces immutability at write time. - Pandas raises this on
groupby/merge/drop_duplicateswhen a column holds lists. Either convert the column to tuples withdf.col.apply(tuple)or usedf.explode("col")to unpack the list into rows.
The rest of this article walks through each case in detail, plus the failure modes most other guides skip.
Why Mutable Types Cannot Be Hashed
Python uses hash values to store and look up dictionary keys and set members efficiently. A hash is a fixed-size integer computed from an object’s contents. For this system to work, the hash must remain constant for the lifetime of the object. If the object changes, its hash would change too, and Python would lose track of it in the internal hash table.
Mutable types like list, dict, and set can change their contents at any time. If Python allowed you to use a list as a dictionary key, you could modify that list later, and the dictionary would break. The key would be “lost” because its hash no longer matches where it was stored.
Immutable types like str, int, float, tuple (if all elements are also hashable), frozenset, and bool have a fixed hash and are safe to use as keys.
Here is a quick reference:
| Type | Hashable? | Immutable Equivalent |
|---|---|---|
list | No | tuple |
dict | No | frozenset of items or json.dumps |
set | No | frozenset |
tuple | Yes* | N/A |
str | Yes | N/A |
int | Yes | N/A |
*A tuple is hashable only if all of its elements are also hashable. A tuple containing a list is not hashable.
This is the same principle behind related Python errors. If you are also dealing with key-related issues, check out the guide on Python KeyError for when a key is missing entirely.
Diagnostic Timeline
The fastest fix is rarely the right fix. Here is how an experienced engineer actually approaches this error when it lands in production.
Minute 0: The reflex. You see unhashable type: 'list' and your hand goes straight to tuple(...). Stop. Casting is the symptom-level fix. If you ship it without understanding why a list reached a hash context, the same bug will reappear in a different place next week, usually with a frozenset or a custom class.
Minute 1: Find the actual call site. The traceback line is correct, but the interesting line is one or two frames up. Re-read the traceback bottom-up and ask: was the unhashable value passed in, or constructed locally? print(type(x), repr(x)) directly above the failing line. If repr shows [...] where you expected "foo", the upstream code is wrong, not the downstream code.
Minute 3: Classify the hash context. There are only four places where Python computes a hash:
- Dictionary key assignment or lookup (
d[key],{key: value},dict.fromkeys) - Set membership (
set.add,s in some_set,{x for x in ...}) - Function caching (
@lru_cache, custom memoization with a dict) - Pandas/NumPy
groupby,merge,drop_duplicates,value_counts
Each category points to a different root cause. A dict-comprehension failure means your value/key axis is inverted. A lru_cache failure means the function signature accepts mutable arguments (almost always a design smell; you cannot safely cache a function whose result depends on a mutable object). A pandas failure means a column that should be scalar is holding a list.
Minute 5: The wrong path: unsafe_hash=True. If you reached a dataclass and your IDE suggests @dataclass(unsafe_hash=True), do not take it. That decorator silently breaks your dict/set the moment any field changes. Use frozen=True instead, or rebuild the object instead of mutating it.
Minute 7: The real root cause. Nine times out of ten, the actual fix is one of:
- A function returning
listwhen the caller expectedtuple(common when refactoring fromreturn a, b, ctoreturn [a, b, c]for “clarity”) - A JSON parse where a single-element field arrives as
["value"]instead of"value" - A pandas column built with
df.groupby("k")["v"].apply(list)that you forgot was a list column - An ORM model with a JSON-typed field where you stored
[1, 2, 3]and now you are trying to dedupe rows
Fix the source of the list, not the hash site. Only fall back to tuple() casting at the hash site when you genuinely need a list everywhere else and a hashable view only at one boundary.
If your unhashable value turned out to be a None you did not expect, the problem is actually upstream null-handling; see Fix: Python TypeError: ‘NoneType’ object is not subscriptable for that class of bug.
When to Use Which Fix
The next eight sections cover the fixes in detail. The table below maps your situation to the recommended fix.
| Your situation | Recommended fix | Why |
|---|---|---|
| List used as dict key or set member | Fix 1: tuple(my_list) | Immutable equivalent |
| Set used as dict key | Fix 2: frozenset(my_set) | Immutable set equivalent |
| Inverting dict where some values are lists | Fix 3: filter or convert via type check | Need per-value handling |
| Deduplicating list of lists | Fix 4: set(tuple(x) for x in data) | Tuples are hashable, sets dedupe |
| Custom class needs to be hashable | Fix 5: @dataclass(frozen=True) or implement __hash__ | Frozen guarantees immutability |
Pandas groupby / merge on list column | Fix 6: df.col.apply(tuple) or df.explode("col") | Tuples sort and hash; explode unpacks |
| Complex nested structure as dict key | Fix 7: json.dumps(obj, sort_keys=True) | Deterministic string key |
Custom class defined __eq__ but not __hash__ | Fix 8: implement both, hash on immutable attrs only | Equality contract requires matching hash |
If multiple rows apply, pick the topmost one.
Fix 1: Convert a List to a Tuple for Dictionary Keys or Set Members
The most common trigger is using a list where a tuple is needed. Lists are mutable, but tuples are not. Convert the list to a tuple:
Problem:
my_dict = {}
key = [1, 2, 3]
my_dict[key] = "value" # TypeError: unhashable type: 'list'Fix:
my_dict = {}
key = [1, 2, 3]
my_dict[tuple(key)] = "value" # WorksThe same applies when adding to a set:
my_set = set()
my_set.add([1, 2, 3]) # TypeError
my_set.add(tuple([1, 2, 3])) # WorksIf your list contains nested lists, you need to convert recursively:
def deep_freeze(obj):
if isinstance(obj, list):
return tuple(deep_freeze(item) for item in obj)
if isinstance(obj, dict):
return tuple(sorted((k, deep_freeze(v)) for k, v in obj.items()))
if isinstance(obj, set):
return frozenset(deep_freeze(item) for item in obj)
return obj
nested = [[1, 2], [3, 4]]
key = deep_freeze(nested) # ((1, 2), (3, 4))An observation from code review: if you find yourself converting lists to tuples frequently in the same function, the data probably wants to be a tuple from the start. I treat tuple(my_list) cropping up more than once or twice as a signal that the producer’s return type is wrong, not the consumer’s hash site.
Fix 2: Use frozenset Instead of set
If you need to use a set as a dictionary key or as a member of another set, use frozenset. A frozenset is the immutable version of a set:
Problem:
my_dict = {}
key = {1, 2, 3}
my_dict[key] = "value" # TypeError: unhashable type: 'set'Fix:
my_dict = {}
key = frozenset({1, 2, 3})
my_dict[key] = "value" # WorksThis is common when you want to group items by a combination of values where order does not matter:
# Group edges in a graph by unordered pairs
edges = [(1, 2), (2, 1), (3, 4), (4, 3)]
unique_edges = set()
for a, b in edges:
unique_edges.add(frozenset([a, b]))
print(unique_edges) # {frozenset({1, 2}), frozenset({3, 4})}frozenset supports the same operations as set (union, intersection, difference) but cannot be modified with add() or remove().
Fix 3: Fix Using Lists as Dictionary Keys
Sometimes you build a dictionary dynamically and accidentally use a list as a key. This is common when reading data from files or APIs where the structure is not obvious.
Problem:
data = {"name": "Alice", "scores": [90, 85, 92]}
# Trying to invert the dictionary
inverted = {v: k for k, v in data.items()}
# TypeError: unhashable type: 'list' (when v is [90, 85, 92])Fix: skip unhashable values:
inverted = {v: k for k, v in data.items() if isinstance(v, (str, int, float, bool, tuple))}Fix: convert lists to tuples:
inverted = {}
for k, v in data.items():
if isinstance(v, list):
inverted[tuple(v)] = k
else:
inverted[v] = kThis kind of type mismatch also shows up when handling data from APIs where a field arrives as a list instead of the scalar you expected.
Fix 4: Fix Using Lists in Sets
Sets require all members to be hashable. If you have a list of lists and want unique sublists, convert the inner lists to tuples first:
Problem:
data = [[1, 2], [3, 4], [1, 2]]
unique = set(data) # TypeError: unhashable type: 'list'Fix:
data = [[1, 2], [3, 4], [1, 2]]
unique = set(tuple(item) for item in data)
print(unique) # {(1, 2), (3, 4)}If you need the results back as lists:
unique_lists = [list(item) for item in unique]
print(unique_lists) # [[1, 2], [3, 4]]A common variation is deduplicating rows from a CSV or database query:
rows = [["Alice", 30], ["Bob", 25], ["Alice", 30]]
unique_rows = list(set(tuple(row) for row in rows))
# [('Alice', 30), ('Bob', 25)]Fix 5: Fix Dataclass Unhashable Error with frozen=True
Python dataclasses are not hashable by default. If you use a dataclass instance as a dictionary key or set member, you get the unhashable error:
Problem:
from dataclasses import dataclass
@dataclass
class Point:
x: int
y: int
points = {Point(1, 2), Point(3, 4)} # TypeError: unhashable type: 'Point'Fix: use frozen=True:
from dataclasses import dataclass
@dataclass(frozen=True)
class Point:
x: int
y: int
points = {Point(1, 2), Point(3, 4)} # Works
my_dict = {Point(1, 2): "origin"} # Also worksSetting frozen=True makes the dataclass immutable. You cannot change its fields after creation, which allows Python to compute a stable hash. If you try to modify a field, you get a FrozenInstanceError.
If you need mutability and hashing (rare, and risky), you can set unsafe_hash=True instead:
@dataclass(unsafe_hash=True)
class Point:
x: int
y: intWarning: Using unsafe_hash=True means you promise not to mutate the object while it is stored in a dict or set. If you do mutate it, the dict or set will silently break. Only use this when you fully control the object’s lifecycle.
This pattern is useful for attribute-related issues on objects. If you are working with classes and hitting attribute errors, see Fix: Python AttributeError: ‘NoneType’ has no attribute.
Fix 6: Fix Pandas Unhashable Type in groupby or merge
Pandas raises this error when a DataFrame column contains lists and you try to use it in groupby, merge, drop_duplicates, or value_counts:
Problem:
import pandas as pd
df = pd.DataFrame({
"name": ["Alice", "Bob", "Alice"],
"tags": [["python", "java"], ["go"], ["python", "java"]]
})
df.groupby("tags").count()
# TypeError: unhashable type: 'list'Fix 1: convert list column to tuples:
df["tags_tuple"] = df["tags"].apply(tuple)
df.groupby("tags_tuple").count()Fix 2: convert list column to a string representation:
df["tags_str"] = df["tags"].apply(lambda x: ",".join(sorted(x)))
df.groupby("tags_str").count()Fix 3: explode the list column and then group:
df_exploded = df.explode("tags")
df_exploded.groupby("tags")["name"].count()The explode approach is often the best choice because it lets you analyze individual tags rather than treating each unique combination as a separate group.
For drop_duplicates, convert the list column to a string first:
df["tags_str"] = df["tags"].apply(str)
df_deduped = df.drop_duplicates(subset=["name", "tags_str"])Pandas index-related errors are another common pain point. If you are hitting index issues, check Fix: Python IndexError: list index out of range.
Fix 7: Use json.dumps for Complex Structures as Keys
When you need to use a complex nested structure (dicts, lists of dicts, mixed types) as a dictionary key, json.dumps with sort_keys=True produces a consistent, hashable string:
Problem:
cache = {}
config = {"host": "localhost", "ports": [8080, 8081]}
cache[config] = "result" # TypeError: unhashable type: 'dict'Fix:
import json
cache = {}
config = {"host": "localhost", "ports": [8080, 8081]}
key = json.dumps(config, sort_keys=True)
cache[key] = "result" # Works (key is a string)Why sort_keys=True matters: Without it, {"a": 1, "b": 2} and {"b": 2, "a": 1} would produce different strings even though they represent the same data. sort_keys=True ensures consistent key ordering.
This technique is especially useful for memoization and caching:
import json
from functools import lru_cache
def make_key(args):
return json.dumps(args, sort_keys=True)
cache = {}
def expensive_query(filters):
key = make_key(filters)
if key in cache:
return cache[key]
result = run_database_query(filters) # Slow operation
cache[key] = result
return resultOne trap I have personally walked into: using str() instead of json.dumps() for this. str({"b": 2, "a": 1}) happily produces different keys across processes if the dict was built in a different order, and your cache hit rate quietly drops by half. json.dumps(sort_keys=True) is deterministic regardless of insertion order; reach for it as a habit.
Note: json.dumps only works with JSON-serializable types (dicts, lists, strings, numbers, booleans, None). If your structure contains custom objects, datetime, or bytes, you need a custom serializer or the deep_freeze approach from Fix 1.
Fix 8: Fix Custom Class Hashing with __hash__ and __eq__
By default, custom classes are hashable (they use the object’s id as the hash). But if you define __eq__ without __hash__, Python sets __hash__ to None, making the class unhashable:
Problem:
class User:
def __init__(self, name, age):
self.name = name
self.age = age
def __eq__(self, other):
return self.name == other.name and self.age == other.age
users = {User("Alice", 30)} # TypeError: unhashable type: 'User'This happens because Python assumes that if two objects can be equal (via __eq__), their hashes must also be equal. Since the default id-based hash does not guarantee this, Python disables hashing entirely.
Fix: implement both __hash__ and __eq__:
class User:
def __init__(self, name, age):
self.name = name
self.age = age
def __eq__(self, other):
if not isinstance(other, User):
return NotImplemented
return self.name == other.name and self.age == other.age
def __hash__(self):
return hash((self.name, self.age))Now instances with the same name and age are considered equal and have the same hash:
user1 = User("Alice", 30)
user2 = User("Alice", 30)
print(user1 == user2) # True
print(hash(user1) == hash(user2)) # True
print({user1, user2}) # {User("Alice", 30)}: one itemRules for correct hashing:
- If
a == b, thenhash(a)must equalhash(b). - Use only immutable attributes in the hash calculation. If you hash based on a mutable attribute and then change it, the object becomes “lost” in the dict or set.
- Return
NotImplementedfrom__eq__when comparing with an incompatible type, so Python can try the other object’s__eq__.
If your class has many fields, use the fields relevant for equality:
class Config:
def __init__(self, host, port, debug):
self.host = host
self.port = port
self.debug = debug # Not relevant for equality
def __eq__(self, other):
if not isinstance(other, Config):
return NotImplemented
return self.host == other.host and self.port == other.port
def __hash__(self):
return hash((self.host, self.port))If you encounter argument-related TypeErrors while working with these classes, the cause is usually a mismatch between the equality semantics and the constructor signature.
Stranger Cases I Have Tracked Down
If the fixes above did not solve your issue, try these additional approaches:
Check for hidden lists in your data. Print the type of the object causing the error:
print(type(your_variable))
print(repr(your_variable))Sometimes a variable you expect to be a string or number is actually a list because of how it was parsed or returned from a function.
Check for nested mutable objects. A tuple containing a list is not hashable:
t = (1, [2, 3])
hash(t) # TypeError: unhashable type: 'list'You need to convert all nested mutable objects. Use the deep_freeze function from Fix 1.
Check for pandas Series being used as keys. If you accidentally pass a pandas Series where a scalar is expected:
# Wrong: passes a Series
df[df["col"]]
# Right: use .values or .tolist() and iterate
for val in df["col"].unique():
print(val)Check your Python version. In Python 3.6+, regular dicts maintain insertion order. But they are still mutable and not hashable. Do not confuse “ordered” with “immutable.”
Use a debugger to find the exact line. Run your script with the -m pdb flag or add a breakpoint:
breakpoint() # Drops into the debugger at this lineThen inspect the variables at the point of failure to see which one is the unexpected list, dict, or set.
Check for @lru_cache on a method with mutable arguments. functools.lru_cache hashes every argument, including self. If any argument is a list, dict, or a regular (non-frozen) dataclass, the first call raises this error. Either convert arguments to tuples before calling, freeze the dataclass with frozen=True, or replace lru_cache with a hand-rolled cache that builds a hashable key from the arguments you actually care about.
Watch for numpy.ndarray masquerading as the trigger. NumPy arrays are not hashable either, but the error message reads unhashable type: 'numpy.ndarray'. The same root-cause logic applies: find the source that built the array and decide whether it should be a tuple, a bytes, or a numpy.ndarray.tobytes() digest. Casting with tuple(arr) only works for 1-D arrays.
Check Python 3.12+ behavior with tuple keys. In Python 3.12 and later, the optimizer is more aggressive about deduplicating tuple constants at compile time. This does not change hashability, but it does mean that tuple(some_list) keys constructed in a tight loop now share the same object identity more often. If you were relying on id() differences between equal tuple keys, that assumption is no longer safe.
Look for a third-party serializer that returns lists. PyYAML, ujson, orjson, and msgpack each have subtle differences in how they decode JSON arrays. orjson decodes everything to lists; ujson matches json. If you switched libraries recently and started hitting this error, that is the cause: the wire format did not change, but the in-memory type did.
If none of these solutions work, the issue is likely in a library you are using. Check the library’s documentation or GitHub issues for known problems with unhashable types in the specific method you are calling.
What Other Tutorials Get Wrong About This Error
Most Python tutorials list the same fixes but frame them in ways that produce subtle bugs.
They recommend unsafe_hash=True for dataclasses as a quick fix. This is genuinely dangerous. unsafe_hash=True allows mutation; the moment a stored object’s hash field changes, the dict or set “loses” the object silently. The bug manifests as wrong lookup results, not as a clean exception. Use frozen=True instead.
They cast at the hash site, not at the source. Adding tuple(my_list) at the point of failure fixes the immediate traceback but lets the same bug reappear elsewhere. The durable fix is to find the upstream code that produced the list and decide whether it should have returned a tuple in the first place.
They miss the pandas explode option. Articles focused on df.col.apply(tuple) to fix groupby on a list column omit that df.explode("col") often produces the analysis the user actually wanted (per-tag rows rather than per-tag-combination rows). The tuple fix is correct but explode is frequently the better answer.
They confuse __eq__ overload with hash failure. Defining __eq__ on a custom class sets __hash__ = None automatically. Tutorials that show __eq__ examples without flagging this trap leave readers with classes that pass == checks but cannot be used in sets or as dict keys.
They omit the lru_cache mutable-argument trap. functools.lru_cache hashes every positional argument including self. A method that takes a list, dict, or non-frozen dataclass fails on the first call. Tutorials that show @lru_cache without warning about argument-hashability send readers into a confusing error path.
They suggest str() for caching keys. str(my_dict) is not deterministic across processes; insertion order influences output. The cache hit rate drops silently. json.dumps(obj, sort_keys=True) is deterministic; tutorials that recommend str() produce caches that look fine in unit tests and fail under production traffic.
Frequently Asked Questions
Why are tuples hashable but lists are not?
Tuples are immutable. Once created, their elements cannot be reassigned, so their hash stays stable for the object’s lifetime. Lists are mutable; the .append(), .pop(), .sort(), and item-assignment operations all change the contents, which would change the hash. Python refuses to compute a hash for mutable types as a safety mechanism.
Why does the error say unhashable type instead of mutable type?
The runtime check is for hashability, not mutability. The two overlap (mutable built-ins are unhashable, immutable built-ins are hashable) but a custom class can be mutable AND hashable (with unsafe_hash=True) or immutable AND unhashable (if __hash__ is set to None). The error reports the actual missing capability.
Is tuple(my_list) always safe as a fix?
Not for nested mutable structures. tuple([1, [2, 3]]) produces (1, [2, 3]), which is still unhashable because one element is a list. Use the deep_freeze recursive helper from Fix 1 for nested cases.
Can I make a list hashable by subclassing it?
Technically yes (override __hash__ and __eq__), but you should not. Lists are designed to be mutable; a hashable list will lose its dict entries the moment someone calls .append(). The right answer is to use a tuple instead.
Why is frozenset hashable but set is not?
Same rule. frozenset is immutable by design; once created, no method can add or remove elements. set has .add(), .remove(), .discard(), .update(), all of which would change the hash. Python provides frozenset exactly so that sets can be used as dict keys.
Does this error happen with NumPy arrays?
Yes, with a different message: unhashable type: 'numpy.ndarray'. The fix differs: tuple(arr) works only for 1-D arrays; for higher dimensions, use arr.tobytes() or convert to a nested tuple manually. The root cause is the same: NumPy arrays are mutable.
Solo developer based in Japan. Every solution is cross-referenced with official documentation and tested before publishing.
Was this article helpful?
Related Articles
Fix: joblib Not Working — Parallel Backends, Memory Cache, and Pickling Errors
How to fix joblib errors — Parallel n_jobs slower than expected, Memory cache miss, backend loky vs threading vs multiprocessing, pickling lambda not supported, dump load file size, and pytest interference.
Fix: Marshmallow Not Working — Schema Errors, Load vs Dump, and Field Validation
How to fix Marshmallow errors — Schema not validated on dump, ValidationError messages format, unknown field handling, missing vs default, post_load object construction, and Marshmallow 3 to 4 migration.
Fix: Pipenv Not Working — Lock File Generation, Shell Activation, and Dependency Resolution
How to fix Pipenv errors — pipenv lock takes forever, Pipfile.lock not generated, shell activation broken, no virtualenv created, dependency conflict, and migration to uv or Poetry.
Fix: Copier Not Working — Template Updates, Question Conditions, and Migrations
How to fix Copier errors — copier.yml not found, conditional questions not appearing, update breaks generated project, migrations between versions, Jinja vs YAML escaping, and answers file conflict.