Back to Blog
Python

Why Python Mutable Objects Are Unhashable

python mutable objects unhashable: Understand why mutable objects are unhashable in Python, how the TypeError arises, and practical ways to work around it.

pythonhashingmutable objectsdictionariessetshashable types
A visual metaphor showing a mutable list being rejected as a dictionary key, with a lock and hash symbol

When you try to use a list as a dictionary key or add a list to a set, Python raises TypeError: unhashable type: 'list'. This error is not a quirk of the language; it follows directly from the contract between hashing and object equality. The phrase python mutable objects unhashable describes a fundamental rule: any object whose value can change after it has been inserted into a hash-based collection cannot safely provide a hash value.

The Hash Contract in Python

Python's dictionaries and sets rely on hash() to compute a fixed-size integer for each object. This hash is used to locate the object in the underlying table. For the collection to work correctly, two requirements must hold:

  1. If two objects compare equal, their hash values must be equal.
  2. The hash value of an object must remain constant for as long as the object lives.

The second requirement is the critical one. If an object's hash changes after it has been placed in a dictionary or set, the collection's internal lookup logic breaks. The object would be stored under one hash but later searched under a different hash, making it effectively lost.

Why Mutable Objects Violate the Contract

Mutable objects—lists, dictionaries, sets, and user-defined classes with mutable attributes—can change their internal state. If the object's __eq__ method compares those mutable fields, then the object's equality relationship changes as the fields change. Consequently, the hash must also change to stay consistent with equality. But a changing hash violates the stability requirement.

Consider a simple class:

class Point: def __init__(self, x, y): self.x = x self.y = y def __eq__(self, other): return self.x == other.x and self.y == other.y def __hash__(self): return hash((self.x, self.y))

If you mutate point.x, the object's hash changes. If that point is already a key in a dictionary, the dictionary will no longer find it. Python prevents this by making the default __hash__ of a mutable object raise TypeError.

The Default Behavior for Built-in Types

Built-in mutable types like list, dict, and set do not implement __hash__. They inherit the default from object, which sets __hash__ to None. When you call hash([]), Python raises TypeError: unhashable type: 'list'. The same applies to dict and set. Immutable types like tuple, frozenset, and str do implement __hash__ because their state cannot change after creation.

Error Scenarios and Their Root Cause

Let's reproduce the error in common situations:

d = {} try: d[['key']] = 'value' except TypeError as e: print(e) # unhashable type: 'list' s = set() try: s.add({'a': 1}) except TypeError as e: print(e) # unhashable type: 'dict'

The error message names the type. It tells you that the object you tried to hash does not provide a valid hash. The root cause is always the same: the object's equality depends on mutable state, so it cannot be safely hashed.

Workarounds: Using Immutable Equivalents

The most direct fix is to use an immutable type that preserves the information you need. For a list, use a tuple:

key = (1, 2, 3) d = {key: 'value'}

For a dictionary, use a frozenset of its items if the order doesn't matter, or a tuple of sorted items if order matters:

config = {'host': 'localhost', 'port': 8080} key = frozenset(config.items())

For a set, use frozenset directly.

These immutable alternatives have stable hashes because their contents cannot change after creation. They work as dictionary keys and set members without any extra code.

Custom Classes: When to Implement __hash__

If you define your own class, you control whether it is hashable. The default behavior is that all user-defined objects are hashable—their hash is based on id() and equality is identity. But if you override __eq__ to compare mutable fields, Python automatically sets __hash__ to None unless you explicitly define it. This is a safety mechanism.

To make a custom class hashable while keeping mutable attributes, you must ensure the hash never changes after the object is created. One common pattern is to make the object immutable in practice:

class ImmutablePoint: def __init__(self, x, y): self._x = x self._y = y self._hash = hash((x, y)) @property def x(self): return self._x @property def y(self): return self._y def __eq__(self, other): return self._x == other._x and self._y == other._y def __hash__(self): return self._hash

Here, the hash is computed once at construction and stored. Because the attributes are read-only, the hash remains valid. This avoids the TypeError while preserving value-based equality.

When Mutability and Hashing Conflict

If you truly need a mutable object to be hashable, you are fighting the design of Python. The conflict is not just an implementation detail; it is a logical necessity. A mutable object that can change its equality relation cannot provide a stable hash. No amount of clever __hash__ implementation can fix that. The only safe options are:

  • Make the object immutable after insertion.
  • Use a wrapper that stores an immutable snapshot for hashing.
  • Avoid using the object as a dictionary key or set member.

A wrapper approach can be useful when you need to look up by value but also mutate the original object:

class MutableKey: def __init__(self, data): self.data = data self._hash = hash(tuple(data)) def __hash__(self): return self._hash def __eq__(self, other): return self.data == other.data

But note that if self.data changes, the hash stays the same, breaking the equality-hash consistency. This is only safe if you never mutate the data after the object is used as a key. The responsibility falls on the developer.

Performance and Memory Implications

Using immutable tuples instead of lists as dictionary keys has a small performance benefit: tuple hashing is cached in CPython, so repeated lookups do not recompute the hash. Lists, if they were hashable, would need to recompute their hash every time, which would be expensive for large lists. This is another reason Python disallows hashing mutable types. When you use a custom class with a precomputed hash, you gain the same caching effect.

Memory usage is also worth considering. A tuple stores a fixed-size array of references, while a list has over-allocation for appends. For use as keys, tuples are more compact. If you need to store many keys, this difference can matter.

Compatibility and Version Behavior

The rule that mutable built-ins are unhashable has been consistent across Python 3.x versions. The error message format may vary slightly, but the behavior is stable. If you are writing code that must work on older Python 2.x, note that Python 2 allowed hashing of lists in some cases (though it was deprecated). In Python 3, the behavior is strict. When migrating code, be aware that any dictionary or set that used a list as a key must be rewritten.

Practical Decision Criteria

When you encounter TypeError: unhashable type, ask yourself what you actually need:

  • If you need a lookup key that represents a sequence of values, use a tuple.
  • If you need a key that represents a set of values, use a frozenset.
  • If you need a key that represents a mapping, convert it to a tuple of sorted items or a frozenset of items.
  • If you need a custom object as a key, make it immutable or precompute its hash and never mutate the fields that affect equality.

Choosing the right approach depends on whether the data is fixed at insertion time. If the data can change, you should think about whether a hash-based collection is the right structure at all. A list of key-value pairs or a custom search structure might be more appropriate.

Handling Unhashable Objects in Real Code

A common pattern in data processing is to deduplicate rows that contain lists. You cannot put a list directly into a set. Instead, convert each row to a tuple before adding it:

rows = [[1, 2], [3, 4], [1, 2]] unique = set(tuple(row) for row in rows)

This works because tuples are hashable. If the row contains nested lists, you need to recursively convert them to tuples. A helper function can do that:

def make_hashable(obj): if isinstance(obj, list): return tuple(make_hashable(item) for item in obj) if isinstance(obj, dict): return tuple(sorted((k, make_hashable(v)) for k, v in obj.items())) if isinstance(obj, set): return frozenset(make_hashable(item) for item in obj) return obj

This function converts any nested mutable structure into an immutable, hashable equivalent. It is useful when you need to store complex objects in a set or use them as dictionary keys.

The Boundary of Hashability in Python

The rule that mutable objects are unhashable is a core part of Python's design. It protects the integrity of hash-based collections and keeps the language predictable. Understanding this rule helps you avoid common errors and choose the right data structures for your use case. When you need to hash a mutable object, convert it to an immutable form or design your class to be immutable from the start. The error message is not a nuisance; it is a guardrail that prevents subtle bugs that would otherwise appear only at runtime.

python mutable objects unhashable: Practical Usage and Code | RYUSLOG DEV