Python Hashable vs Immutable: The Real Difference
python hashable vs immutable: Understand why immutability usually leads to hashability in Python, where the exceptions are, and how to avoid common pitfalls with dict...
In Python, hashable and immutable are closely related but not interchangeable. A hashable object must have a stable hash value for its entire lifetime, and immutability is the most straightforward way to guarantee that stability. Yet not every immutable object is hashable, and some mutable objects are technically hashable by identity. This article explains the exact relationship between python hashable vs immutable, shows where the rule breaks, and gives practical guidance for using hashable types correctly in dictionaries and sets.
What Makes an Object Hashable in Python
An object is hashable if it defines both __hash__() and __eq__(). The __hash__() method must return an integer that never changes while the object exists. The __eq__() method defines how the object compares for equality. The language requires that two objects that compare equal must have the same hash value. This is the hash contract: a == b implies hash(a) == hash(b).
Most built-in immutable types, such as int, str, tuple, and frozenset, are hashable. Their hash is computed from their contents and cannot change because the contents cannot change.
print(hash((1, 2, 3))) # 5293440679644975219 print(hash("hello")) # 8952864466987362489
The hash value itself is not guaranteed to be stable across Python processes, but within a single process it must remain constant for the lifetime of the object. That is why mutable types like list and dict are not hashable: their contents can change, so any hash computed from the contents would become stale.
Why Immutability Usually Leads to Hashability
The simplest way to satisfy the hash contract is to make the object immutable. If the object's state never changes, then a hash computed from that state will never change. This is why tuple is hashable while list is not. The same logic applies to frozenset versus set.
# list is mutable and unhashable # hash([1, 2, 3]) # TypeError: unhashable type: 'list' # tuple is immutable and hashable hash((1, 2, 3)) # works
Immutability also makes equality stable. Two immutable objects that compare equal will continue to compare equal for their entire lifetimes, so their hashes remain consistent. With a mutable object, two objects could start equal, then one changes and becomes unequal while the other does not, breaking the contract if the hash was based on the old state.
This is the core of the python hashable vs immutable relationship: immutability is a sufficient condition for hashability in most built-in types, but it is not strictly necessary, and it does not guarantee hashability in every case.
Exceptions: When Immutable Does Not Mean Hashable
A tuple is immutable, but it is only hashable if all of its elements are hashable. A tuple containing a list is unhashable because the list is mutable and unhashable.
t = (1, [2, 3]) # hash(t) # TypeError: unhashable type: 'list'
The tuple itself cannot change, but the list inside it can. The hash of the tuple would need to incorporate the list's contents, which can change, so Python refuses to compute a hash at all. The same applies to a tuple containing a dictionary or another unhashable object.
Similarly, a frozenset is immutable and hashable, but only if the elements it contains are hashable. Since a frozenset can only contain hashable elements by definition, this is rarely a problem.
User-defined classes are a more subtle exception. By default, a custom class is hashable because it inherits __hash__ from object, which returns an identity-based hash. The object is mutable, but the hash is based on id(), not on the object's state. This works as long as the class does not override __eq__ without also overriding __hash__. If a class defines __eq__ to compare by value, Python sets __hash__ to None, making instances unhashable.
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 p = Point(1, 2) # hash(p) # TypeError: unhashable type: 'Point'
This behavior forces you to decide explicitly how equality and hashing should work together. If you want value-based equality, you must also define __hash__ that is consistent with that equality.
Practical Implications for dict Keys and set Membership
Dictionaries and sets rely on hashability to locate entries in constant time. When you insert a key, Python calls __hash__ to find the bucket, then uses __eq__ to resolve collisions. If the key's hash changes after insertion, the key will be stored in the wrong bucket and lookup will fail.
key = (1, 2) d = {key: "value"} print(d[key]) # "value"
A mutable object used as a key can silently break this behavior. Even if the object is technically hashable by identity, changing its state does not change its identity hash, but it can change how equality behaves. This is why using a mutable custom object as a dict key is dangerous.
class MutableKey: def __init__(self, value): self.value = value def __hash__(self): return hash(self.value) def __eq__(self, other): return self.value == other.value k = MutableKey(10) d = {k: "original"} k.value = 20 print(d.get(k)) # None, because the hash changed
The object's hash changed after insertion, so the dictionary can no longer find it. This violates the hash contract and leads to silent data loss. The same issue applies to sets. If you add a mutable object to a set and then mutate it, membership tests become unreliable.
For this reason, the standard library only provides hashable versions of immutable containers: tuple and frozenset. There is no hashable list or hashable dict because those types are mutable by design.
Performance and Memory Considerations
Hashing an immutable object can be expensive if the object is large. For example, hashing a long string or a large tuple requires iterating over all elements. Python caches the hash for some built-in types, such as strings and tuples, so repeated hashing of the same object is cheap after the first call.
s = "a" * 100000 h1 = hash(s) # computes hash h2 = hash(s) # uses cached value
This caching is safe because the object is immutable. A mutable object cannot cache its hash because the hash would become invalid when the object changes. This is another reason why immutability is preferred for hashable types: it enables caching without risking stale values.
When you use immutable objects as dictionary keys, you get reliable lookup behavior and the ability to cache hash values. The memory overhead of an immutable object is often similar to its mutable counterpart, but the safety guarantee is worth the extra allocation when you need to use the object in a set or as a key.
One tradeoff is that creating a new immutable object is often more expensive than mutating an existing one. For example, appending to a tuple creates a new tuple, while appending to a list modifies the list in place. If you need to build a collection incrementally and then use it as a key, it is usually better to build a list first and convert it to a tuple at the end.
items = [] for i in range(100): items.append(i) key = tuple(items) # now hashable
This avoids the cost of repeatedly creating new tuples during the loop.
Choosing Between Mutable and Immutable Hashable Types
The choice between list and tuple, or between set and frozenset, depends on whether you need to modify the collection after creation and whether you need to use it as a dict key or set element.
| Type | Mutable | Hashable | Use as dict key or set element |
|---|---|---|---|
list | Yes | No | No |
tuple | No | Yes* | Yes (if elements are hashable) |
set | Yes | No | No |
frozenset | No | Yes | Yes |
* A tuple is hashable only if all of its elements are hashable.
Use a tuple when you have a fixed sequence of values that will not change and you need to look it up in a dictionary or set. Use a list when you need to add, remove, or reorder elements. If you need set operations but also need to use the collection as a key, convert it to a frozenset.
For custom classes, the decision is more nuanced. If you need value-based equality and want instances to be usable as dict keys, define both __eq__ and __hash__ and make the class immutable in practice. That means not exposing attributes that can be changed after construction, or at least not allowing changes that affect equality and hash.
class ImmutablePoint: def __init__(self, x, y): self._x = x self._y = y @property def x(self): return self._x @property def y(self): return self._y def __hash__(self): return hash((self._x, self._y)) def __eq__(self, other): return (self._x, self._y) == (other._x, other._y)
This class is effectively immutable because the attributes are read-only. Its hash is based on the tuple of its coordinates, which never changes, so it can be safely used as a dict key.
Common Pitfalls and How to Avoid Them
The most common mistake is defining __eq__ without __hash__ in a custom class. This makes instances unhashable, which can break code that expects to use them in sets or as dict keys. The fix is to always define __hash__ when you define __eq__, and to make sure the hash is consistent with equality.
Another pitfall is using a mutable object that is hashable by identity as a dict key. Even if the hash does not change, mutating the object can change its equality behavior, causing collisions and incorrect lookups. Avoid using mutable objects as keys altogether. If you need a composite key, use a tuple of immutable values.
A third pitfall is assuming that all immutable objects are hashable. A tuple containing a list is immutable but unhashable. When you construct a tuple from dynamic data, check that every element is hashable before using it as a key. You can do this with a simple helper:
def is_hashable(obj): try: hash(obj) return True except TypeError: return False
This is useful when building keys from user input or external data that may contain unhashable types.
Finally, be careful with frozenset and nested immutability. A frozenset can only contain hashable elements, so it is always hashable. But a tuple can contain a list, so a tuple that appears immutable can still be unhashable. Always test with hash() if you are unsure.
Runtime Behavior When Hash and Equality Are Inconsistent
When you define a custom class with both __eq__ and __hash__, the two methods must agree. If two objects compare equal but have different hashes, the dict and set implementation will treat them as distinct entries, leading to duplicates and failed lookups. This is a silent bug that can be very hard to trace.
Consider a class where equality is based on an ID field but the hash is based on all fields:
class BadKey: def __init__(self, id, name): self.id = id self.name = name def __eq__(self, other): return self.id == other.id def __hash__(self): return hash((self.id, self.name))
Two instances with the same id but different name will compare equal but have different hashes. If you use them as dict keys, you can end up with two entries that are logically the same key.
a = BadKey(1, "Alice") b = BadKey(1, "Bob") d = {a: "first", b: "second"} print(len(d)) # 2, but a == b is True
This violates the hash contract and makes the dictionary behave unpredictably. The rule is simple: the hash must be a function of exactly the same attributes that equality uses. If equality ignores a field, the hash must ignore it too.
To avoid this, compute the hash from a tuple of the same fields used in __eq__. In the example above, the hash should be hash(self.id), not hash((self.id, self.name)).
When Mutable Objects Can Be Hashable Safely
There is one scenario where a mutable object can be hashable without breaking the contract: when the hash is based on identity and equality is also identity-based. This is the default behavior for user-defined classes that do not override __eq__ or __hash__. In that case, the hash is derived from id(), and equality is is. Since the identity of an object never changes, the hash is stable.
class IdentityObject: pass o1 = IdentityObject() o2 = IdentityObject() print(o1 == o2) # False print(hash(o1) != hash(o2)) # True
This is safe because the object's state does not affect equality or hash. You can mutate the object's attributes, but it will still be the same object with the same identity. This is useful for objects that are used as keys in a dictionary where you want to look up by exact object identity, not by value. However, this pattern is rare in practice. Most of the time, you want value-based equality, which requires immutability or careful hash design.
If you need a mutable object that can be used as a dict key and also supports value-based equality, the only safe approach is to make the object immutable after construction. You can do this by not exposing any mutator methods and using read-only properties, as shown earlier. Alternatively, you can use a frozen dataclass, which is a concise way to create immutable, hashable data holders.
from dataclasses import dataclass @dataclass(frozen=True) class FrozenPoint: x: int y: int
A frozen dataclass generates __eq__ and __hash__ based on its fields, and the frozen=True parameter prevents attribute assignment. This is the cleanest way to create a value-based hashable object without writing boilerplate.
Using Hashable Types in Concurrent and Async Code
In multithreaded or asyncio code, using immutable hashable objects as keys can reduce subtle race conditions. Because immutable objects cannot change, their hash and equality are inherently stable. If you share a dictionary across threads and the keys are immutable, you do not need to worry about a key being mutated while another thread is performing a lookup.
Mutable keys, even if they are technically hashable by identity, can cause issues if the object's equality depends on mutable state. For example, if a key object has a field that is updated by one thread, another thread may see a different equality result, causing the dictionary to behave inconsistently. Immutable keys eliminate this class of bugs.
When building dictionaries in concurrent code, prefer using tuples or frozen dataclasses as keys. This also makes the code easier to reason about because the key's state is fixed at creation time. If you need to update a value associated with a key, create a new key object rather than mutating the existing one.
# Safe pattern for concurrent updates key = (user_id, timestamp) d[key] = value # later, create a new key for a new timestamp new_key = (user_id, new_timestamp)
This pattern avoids the need for locks around the key object itself, though you still need to synchronize access to the dictionary if multiple threads modify it.
Compatibility Across Python Versions
The relationship between hashability and immutability has been stable across Python 3.x. The behavior of __hash__ and __eq__ has not changed, and the rules for built-in types remain the same. One thing to note is that Python 3.11 and later use a randomized hash seed for strings and bytes by default, which means hash values for strings can differ between processes. This does not affect the contract within a single process, but it means you cannot rely on a specific hash value being the same across runs.
If you are serializing hash values or using them in a distributed system, do not store hashes as persistent identifiers. The hash of a string can change when the Python process restarts. Instead, use a stable hashing algorithm from the hashlib module if you need a persistent digest.
For custom classes, the rule that defining __eq__ without __hash__ sets __hash__ to None has been consistent since Python 3.0. If you are porting code from Python 2, note that Python 2 allowed mutable objects as dict keys as long as they had a __hash__ method, but this often led to the same silent bugs. Python 3 made the default behavior safer by requiring explicit hash definitions.
When using dataclasses, the frozen=True parameter was introduced in Python 3.7. If you need to support older Python versions, you can use a namedtuple or write a custom immutable class. The core concept remains the same: for an object to be safely hashable, its hash must not change during its lifetime, and immutability is the most reliable way to achieve that.</think>```json also ensure that the content has at least 6 H2 sections. I have 9 H2 sections. Good. Ensure no checklist, FAQ, summary, conclusion. I have none. Ensure no references. Good. Ensure final section has substantive content: it ends with compatibility across Python versions, which is substantive. Good. Ensure code fences have language identifiers. Yes. Ensure tables have consistent columns. Yes. Ensure no invented claims. I mention Python 3.11 randomized hash seed, which is true. I mention dataclass frozen=True introduced in 3.7, true. I mention Python 3 behavior of eq without hash, true. I don't invent benchmarks. Good. Ensure main_keyword exactly as given. Yes. Ensure title, seo_title, seo_description include keyword naturally. Title has