Python dictionaries and sets make lookups feel simple: give them a key or value, and they can usually find it quickly. That convenience depends on a contract that becomes important as soon as you create your own value-like classes.
A dictionary key is not located by equality alone. Python first uses the object’s hash value to narrow the search, then uses equality to distinguish candidates that land in the same area of the hash table.
That creates two rules a hashable object must obey:
if a == b, then hash(a) == hash(b)
and
an object's hash must not change while it is being used as a hash keyBreaking either rule can produce lookups that appear inconsistent even though the dictionary or set itself is working correctly.
This article develops a practical mental model for __eq__() and __hash__(), then shows how to design domain objects that are safe to use as dictionary keys and set members.
Start with what a hash is for
A hash is an integer derived from an object. Hash-based collections use it as an indexing aid.
Consider a dictionary lookup conceptually:
key
|
v
hash(key)
|
v
candidate bucket
|
v
compare candidate keys with ==
|
v
matching entryThe exact internal representation is an implementation detail. The important semantic point is that hashing gets Python to a small set of candidates, while equality determines whether a candidate represents the requested key.
Two unequal objects are allowed to have the same hash. That situation is called a hash collision, and dictionaries and sets are designed to handle it.
Two equal objects, however, must not have different hashes. If they did, Python could search different candidate locations for objects that your equality logic says represent the same value.
Hashability is a behavioral contract
An object is hashable when it has a hash value that remains stable during its lifetime and can participate in equality comparisons.
Hashable objects can be used as:
- dictionary keys;
- set members;
- elements of a
frozenset, provided all nested elements are also hashable.
Many immutable built-in values are hashable:
hash(42)
hash("order-123")
hash((10, 20))
hash(frozenset({"read", "write"}))Mutable containers such as lists, dictionaries, and sets are intentionally unhashable:
hash([10, 20]) # raises TypeErrorThe issue is not that mutability is always forbidden in a hashable object. The real requirement is narrower: the state that determines equality and hashing must remain stable while the object is used as a hash key.
In practice, making value-like hash keys immutable is usually the clearest way to satisfy that requirement.
User-defined objects are hashable by default
A simple class with no custom equality method inherits equality and hashing behavior from object:
class Job:
pass
first = Job()
second = Job()
assert first != secondThe exact hash values are not important. What matters is that two distinct instances normally compare unequal and are hashable by identity-oriented default behavior.
This default is appropriate for entities whose identity is the object instance itself.
For example, two open network connection objects might contain similar configuration but still represent different live resources. Treating them as distinct by identity is reasonable.
The situation changes when you define value equality.
Overriding equality changes the hashing decision
Suppose a Coordinate should compare by its data:
class Coordinate:
def __init__(self, x, y):
self.x = x
self.y = y
def __eq__(self, other):
if not isinstance(other, Coordinate):
return NotImplemented
return self.x == other.x and self.y == other.yNow these two instances represent the same value:
a = Coordinate(10, 20)
b = Coordinate(10, 20)
assert a == bBecause the class overrides __eq__() but does not define __hash__(), Python makes its instances unhashable:
hash(a) # raises TypeErrorThat behavior protects you from accidentally inheriting identity hashing while using value equality.
Identity hashing would violate the core contract because two equal coordinates could then have unrelated hashes.
Build the hash from the same state used for equality
If a value object should be hashable, base __hash__() on the same logical components used by __eq__().
A common pattern is to hash a tuple:
class Coordinate:
def __init__(self, x, y):
self.x = x
self.y = y
def __eq__(self, other):
if not isinstance(other, Coordinate):
return NotImplemented
return self.x == other.x and self.y == other.y
def __hash__(self):
return hash((self.x, self.y))Now equal values also have equal hashes:
a = Coordinate(10, 20)
b = Coordinate(10, 20)
assert a == b
assert hash(a) == hash(b)
locations = {a: "warehouse"}
assert locations[b] == "warehouse"The lookup through b works even though the dictionary was populated with a, because equality and hashing describe the same logical value.
Do not hash unrelated state
This implementation is wrong:
def __hash__(self):
return hash(id(self))for a class whose equality compares x and y.
Two equal coordinates can have different identities, so their hashes can differ. The hash function must reflect the equality semantics, not merely produce an integer.
Mutable equality fields can corrupt lookup behavior
The most dangerous mistake is allowing the hash to change after insertion.
Consider this class:
class CustomerCode:
def __init__(self, code):
self.code = code
def __eq__(self, other):
if not isinstance(other, CustomerCode):
return NotImplemented
return self.code == other.code
def __hash__(self):
return hash(self.code)At first it behaves correctly:
customer = CustomerCode("C-100")
records = {customer: "active"}
assert records[customer] == "active"Now mutate the field used by the hash:
customer.code = "C-200"The dictionary entry was indexed using the old hash, but future lookups compute the new hash.
At that point, behavior such as this can occur:
customer in records # may be FalseThe dictionary still contains a reference to the object. The problem is that the object’s lookup identity changed after insertion.
This is why a mutable value that participates in equality is usually a poor dictionary key.
A stable hash must match stable equality
It may seem possible to fix the previous class by caching the original hash:
class CustomerCode:
def __init__(self, code):
self.code = code
self._hash = hash(code)
def __hash__(self):
return self._hashBut if code remains mutable and equality still compares the current code, the class can violate the other part of the contract.
Imagine two objects:
a started as C-100, then changed to C-200
b was created as C-200
a == b -> True
hash(a) -> hash("C-100")
hash(b) -> hash("C-200")Now equal objects can have different hashes.
Caching a hash is safe only when the equality-defining state is itself stable.
Prefer immutable value objects for hash keys
For small domain values, immutability makes the contract much easier to reason about.
A frozen dataclass is one convenient option:
from dataclasses import dataclass
@dataclass(frozen=True)
class CustomerCode:
value: strInstances compare by their dataclass fields, and a frozen dataclass with the normal equality behavior can generate an appropriate hash:
first = CustomerCode("C-100")
second = CustomerCode("C-100")
assert first == second
assert hash(first) == hash(second)
records = {first: "active"}
assert records[second] == "active"The important design benefit is not the decorator itself. It is that the fields defining the value cannot be reassigned through normal attribute assignment, so equality and hashing remain aligned.
A manually written immutable class can satisfy the same contract.
Immutability must extend to hash-participating values
A frozen outer object does not magically make every nested object immutable or hashable.
This fails:
from dataclasses import dataclass
@dataclass(frozen=True)
class Rule:
name: str
labels: list[str]
rule = Rule("publish", ["public"])
hash(rule) # raises TypeErrorThe generated hash eventually needs to hash labels, and lists are unhashable.
If the collection is logically part of an immutable value, represent it with an immutable, hashable type:
from dataclasses import dataclass
@dataclass(frozen=True)
class Rule:
name: str
labels: tuple[str, ...]
rule = Rule("publish", ("public", "reviewed"))
assert isinstance(hash(rule), int)For unordered unique values, frozenset may be a better representation than a tuple:
permissions = frozenset({"read", "write"})Choose the type that matches the domain semantics, not merely one that happens to be hashable.
Equality should return NotImplemented for unsupported types
When a custom equality method cannot meaningfully compare the other operand, return NotImplemented rather than immediately returning False.
class Coordinate:
def __init__(self, x, y):
self.x = x
self.y = y
def __eq__(self, other):
if not isinstance(other, Coordinate):
return NotImplemented
return self.x == other.x and self.y == other.yNotImplemented lets Python give the other operand a chance to participate in the comparison and then apply its normal fallback behavior.
This matters more for equality correctness than for hashing itself, but the two methods form one value contract. Ambiguous equality semantics make it harder to define a correct hash.
Hash collisions are normal
A correct __hash__() does not need to assign a unique integer to every unequal value.
This deliberately poor hash is still logically valid:
class SlowKey:
def __init__(self, value):
self.value = value
def __eq__(self, other):
if not isinstance(other, SlowKey):
return NotImplemented
return self.value == other.value
def __hash__(self):
return 1Every instance collides, but equality can still distinguish them.
The problem is performance, not correctness. A hash function that creates excessive collisions gives the collection less useful information for narrowing candidate keys.
For composite values, hashing a tuple of the equality-defining components is usually clearer and more robust than inventing custom integer arithmetic:
def __hash__(self):
return hash((self.account_id, self.region))Do not depend on the resulting integer being stable across separate Python processes. Some built-in hashes, including string and bytes hashes, are intentionally randomized between interpreter runs.
Do not persist Python hash values as durable identifiers
A Python hash is intended for in-process hash-based collections. It is not a content checksum, database identifier, cache key format, or cryptographic digest.
This is a fragile persistence strategy:
stored_id = hash(("user-42", "eu-west"))The value may differ across interpreter runs or builds depending on the data involved and runtime characteristics.
If you need a durable identifier, define one explicitly from stable domain data.
If you need an integrity or cryptographic digest, use the appropriate hashing API, such as a member of Python’s hashlib module, for that separate purpose.
__hash__() and cryptographic hashing solve different problems despite sharing the word “hash.”
Sets follow the same contract
Set membership uses the same hash/equality relationship as dictionary keys.
allowed = {
CustomerCode("C-100"),
CustomerCode("C-200"),
}
assert CustomerCode("C-100") in allowedThis also explains why sets cannot contain lists:
{["read", "write"]} # raises TypeErrorIf you need a set of sets, use frozenset for the inner values:
groups = {
frozenset({"read", "write"}),
frozenset({"read"}),
}The inner frozenset values are immutable and hashable when their members are hashable.
Be deliberate with inheritance
Hashing and equality become harder to reason about when subclasses change value semantics.
Suppose a base class compares only an identifier while a subclass adds fields that supposedly participate in equality. If equality is no longer symmetric across the class hierarchy, dictionary and set behavior becomes difficult to predict.
Before making a value class extensible, decide whether subclasses are allowed to represent the same equality domain.
For many small immutable value objects, a simple final-by-convention class is easier to reason about than an inheritance hierarchy with changing equality rules.
If inheritance is required, test equality in both operand orders and verify that every pair that compares equal also produces the same hash.
Check hashability through the protocol
When an API genuinely requires hashable input, collections.abc.Hashable can express that requirement:
from collections.abc import Hashable
def require_hashable(value):
if not isinstance(value, Hashable):
raise TypeError("value must be hashable")For ordinary application code, directly attempting the operation can also be clearer:
try:
index[value] = result
except TypeError:
...Which style is better depends on whether hashability is part of the API contract or simply a property needed at one operation.
One detail matters if you implement custom classes: explicitly setting __hash__ = None marks a class as unhashable in a way the Hashable protocol recognizes.
Common mistakes
Defining value equality but keeping identity hashing
Python helps prevent this when you override __eq__() without defining __hash__(). Do not work around that protection unless you can state a coherent hash contract.
Hashing mutable fields
If equality-defining fields can change, the object can move logically while a dictionary still stores it according to an older hash.
Making the hash stable while equality changes
A cached or identity-based hash does not fix mutable value equality. Equal objects must still have equal hashes.
Assuming every immutable-looking object is hashable
A tuple containing a list is still unhashable because one of its elements cannot be hashed.
Treating hash() as a durable fingerprint
Python’s object hash is for hash-table behavior, not cross-process identity, integrity checking, or cryptography.
When custom hashability is appropriate
A custom value class is a good hash key when:
- equality has a precise domain meaning;
- the fields used for equality remain stable;
- the hash is derived from those same fields;
- using equivalent instances interchangeably as keys is useful to callers.
Examples include coordinates, immutable configuration keys, normalized identifiers, version tuples, and composite lookup keys.
If the object represents a mutable entity whose state evolves over time, use a stable immutable identifier as the dictionary key instead:
customers_by_id[customer.id] = customerThat is often simpler than making the entire mutable entity hashable.
Keep equality and hashing as one design decision
__eq__() and __hash__() should not be designed independently.
First decide what makes two instances represent the same value. Then decide whether that value is stable enough to participate safely in hash-based collections. If it is, derive the hash from the same immutable state. If it is not, leave the object unhashable and use a separate stable key.
That approach turns dictionary and set behavior from something mysterious into a predictable consequence of the value model you chose.