Skip to content

Fix: Python TypeError: unhashable type: 'list'

FixDevs · (Updated: )

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:

  1. Hashable means “immutable with a stable __hash__.” Lists, dicts, and sets are mutable, so Python refuses to compute a hash for them. The Python hashable glossary entry and the __hash__ data model docs are the canonical sources.
  2. 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 with deep_freeze (Fix 1).
  3. Use frozenset when order does not matter. If you need a set as a dict key, frozenset(my_set) is the immutable equivalent.
  4. Dataclasses need @dataclass(frozen=True), not unsafe_hash=True. The latter silently breaks dicts and sets the moment any field changes; the former enforces immutability at write time.
  5. Pandas raises this on groupby / merge / drop_duplicates when a column holds lists. Either convert the column to tuples with df.col.apply(tuple) or use df.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:

TypeHashable?Immutable Equivalent
listNotuple
dictNofrozenset of items or json.dumps
setNofrozenset
tupleYes*N/A
strYesN/A
intYesN/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:

  1. Dictionary key assignment or lookup (d[key], {key: value}, dict.fromkeys)
  2. Set membership (set.add, s in some_set, {x for x in ...})
  3. Function caching (@lru_cache, custom memoization with a dict)
  4. 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 list when the caller expected tuple (common when refactoring from return a, b, c to return [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 situationRecommended fixWhy
List used as dict key or set memberFix 1: tuple(my_list)Immutable equivalent
Set used as dict keyFix 2: frozenset(my_set)Immutable set equivalent
Inverting dict where some values are listsFix 3: filter or convert via type checkNeed per-value handling
Deduplicating list of listsFix 4: set(tuple(x) for x in data)Tuples are hashable, sets dedupe
Custom class needs to be hashableFix 5: @dataclass(frozen=True) or implement __hash__Frozen guarantees immutability
Pandas groupby / merge on list columnFix 6: df.col.apply(tuple) or df.explode("col")Tuples sort and hash; explode unpacks
Complex nested structure as dict keyFix 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 onlyEquality 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"  # Works

The same applies when adding to a set:

my_set = set()
my_set.add([1, 2, 3])        # TypeError
my_set.add(tuple([1, 2, 3])) # Works

If 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"  # Works

This 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] = k

This 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 works

Setting 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: int

Warning: 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 result

One 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 item

Rules for correct hashing:

  1. If a == b, then hash(a) must equal hash(b).
  2. 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.
  3. Return NotImplemented from __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 line

Then 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.

F

FixDevs

Solo developer based in Japan. Every solution is cross-referenced with official documentation and tested before publishing.

Was this article helpful?

Related Articles