Python Mutable vs Hashable: What Every Developer Should Know
python mutable vs hashable: Understand why mutable objects are not hashable in Python, how hashability affects dict keys and set membership, and when to override __has...
python mutable vs hashable requires a clear understanding of the core syntax, runtime behavior, and practical implementation patterns demonstrated in the examples below.
Python's distinction between mutable and hashable objects is not just a theoretical language detail—it directly determines what you can use as a dictionary key, what can be stored in a set, and how your custom classes behave in hash-based collections. The rule is simple: an object must be hashable to be used as a dict key or set member, and in Python, mutable objects are not hashable by default. This article explains why that is, what makes an object hashable, and how to handle cases where you need custom behavior.
The Core Distinction Between Mutability and Hashability
Mutability means an object's state can change after creation. Lists, dictionaries, and sets are mutable; integers, strings, and tuples are immutable. Hashability, on the other hand, is the ability to produce a stable integer hash value that is consistent with equality. An object is hashable if it has a __hash__ method that returns an integer and its hash value never changes during its lifetime. In CPython, the default __hash__ for user-defined classes is based on id() and is stable because the object's memory address does not change. However, the language specification requires that if two objects are equal, they must have the same hash value. This is where mutability becomes a problem.
Why Hashability Depends on Immutability
Consider a list used as a dictionary key. If you could put a list in a dict and then modify it, the hash value would need to change to reflect the new contents, but the dict already placed the key in a bucket based on its original hash. When you later look up that key, the dict would compute a new hash, look in a different bucket, and fail to find the entry. To prevent this inconsistency, Python makes mutable built-in types unhashable. The list type defines __hash__ as None, and attempting to use a list as a dict key raises TypeError: unhashable type: 'list'. The same applies to dict and set. Immutable types like tuple are hashable, but only if all their elements are hashable. A tuple containing a list is still unhashable because the tuple's hash is derived from its elements' hashes.
What Happens When You Try to Use a Mutable Object as a Dict Key
The error is immediate and clear. Here is a minimal example:
my_dict = {} my_list = [1, 2, 3] try: my_dict[my_list] = "value" except TypeError as e: print(e) # unhashable type: 'list'
The same occurs with sets:
try: s = set() s.add([1, 2]) except TypeError as e: print(e) # unhashable type: 'list'
This behavior is intentional. It prevents the situation where a key's hash changes after insertion, which would corrupt the dict's internal structure and make lookups unreliable. The only safe way to use a mutable object as a key is to convert it to an immutable representation first, such as a tuple or a frozenset.
Hashable Mutable Types and How to Handle Them
Some mutable types are hashable because they do not change their hash when their contents change. The most common example is a user-defined class that does not override __eq__ and __hash__. By default, such objects are hashable based on identity, and mutating their attributes does not affect their hash. However, this is often not what you want. If you override __eq__ to compare attribute values, you must also override __hash__ to be consistent. The rule is: if two objects are equal, they must have the same hash. If you define __eq__ but leave __hash__ as None, your class becomes unhashable, which is the default behavior in Python 3. For example:
class Point: def __init__(self, x, y): self.x = x self.y = y def __eq__(self, other): return isinstance(other, Point) and self.x == other.x and self.y == other.y p1 = Point(1, 2) p2 = Point(1, 2) print(p1 == p2) # True print(hash(p1)) # TypeError: unhashable type: 'Point'
To make Point hashable, you need to provide a __hash__ that is consistent with __eq__. A common pattern is to hash a tuple of the attributes:
class Point: def __init__(self, x, y): self.x = x self.y = y def __eq__(self, other): return isinstance(other, Point) and self.x == other.x and self.y == other.y def __hash__(self): return hash((self.x, self.y))
Now Point is hashable and can be used in sets and as dict keys. But note: if you mutate p.x after inserting it into a dict, the hash will change and the object will be lost. This is why it is generally recommended to make your hashable classes immutable, or at least treat them as immutable once they are used in hash-based collections.
Using Custom Classes: When to Override hash and eq
The decision to override __hash__ and __eq__ depends on how you intend to use the class. If you only need identity-based equality, you can rely on the default object implementations, which are hashable. If you need value-based equality, you must implement both methods together. The Python documentation states that if you define __eq__ but not __hash__, the class becomes unhashable. This is a safety measure because the default hash is based on identity, which would violate the equality-hash contract. When you do override both, ensure that the hash is computed from the same attributes that determine equality. A common mistake is to include mutable attributes in the hash, which can cause the hash to change. The safest approach is to use only immutable attributes for both __eq__ and __hash__. If you need to include mutable state, consider making the class immutable by using @dataclass(frozen=True) or by not exposing mutators.
Performance and Memory Implications of Hashability
The performance of dict and set operations depends on the quality of the hash function. A good hash distributes objects uniformly across buckets, minimizing collisions. For immutable built-in types like integers and strings, Python uses highly optimized hash functions. For custom classes, the hash function you provide directly affects lookup speed. If your __hash__ is expensive (e.g., it hashes a large tuple), every insertion and lookup will pay that cost. In contrast, using an immutable tuple as a key is often faster than a custom object because the tuple's hash is cached after the first computation. Python caches the hash of strings and bytes, but not of tuples. However, tuple hashing is still efficient for small sizes. When you design a class to be hashable, keep the hash computation cheap and avoid including fields that are large or expensive to hash. Also, be aware that if you use a mutable object that is hashable by identity, you lose the ability to look it up by value, which may lead to subtle bugs if you expect value semantics.
Practical Guidelines for Choosing Hashable Types
When you need to use a collection as a dict key or set member, prefer immutable types. Use tuple for fixed sequences, frozenset for unordered collections, and str or int for simple values. If you have a custom class, make it immutable and implement __hash__ and __eq__ together. If you must use a mutable object, consider using a wrapper that provides a stable hash, but be careful: if the object's state changes, the hash changes, and you will not be able to retrieve it from the dict or set. The standard library provides dataclasses with frozen=True to create immutable data classes that are automatically hashable. For example:
from dataclasses import dataclass @dataclass(frozen=True) class Point: x: int y: int p = Point(1, 2) d = {p: "origin"} print(d[p]) # origin
This generates both __eq__ and __hash__ based on the fields, and the frozen=True prevents mutation. If you need to update a point, create a new instance instead of modifying the existing one. This pattern keeps your hash-based collections consistent and avoids the runtime errors and data loss that occur when a hashable object's state changes after insertion. The choice between a tuple and a custom immutable class often comes down to readability and whether you need additional methods. Tuples are lightweight and fast, but they lack named fields. A frozen dataclass gives you named attributes and can include methods, at the cost of a bit more overhead. For most applications, either works well as long as you respect the hashability contract.