Python __eq__ vs __hash__: The Contract You Must Follow
python **eq** vs **hash**: Understand the relationship between __eq__ and __hash__ in Python, why they must be consistent, and how to implement them correctly for hash...
python eq vs hash requires a clear understanding of the core syntax, runtime behavior, and practical implementation patterns demonstrated in the examples below.
In Python, the relationship between __eq__ and __hash__ is a contract that determines whether objects can be used as keys in dictionaries or members of sets. If you define __eq__ without __hash__, Python makes instances unhashable by setting __hash__ to None. This article explains the rules, common mistakes, and how to implement both methods correctly.
The Contract Between __eq__ and __hash__
The core rule is simple: if two objects compare as equal using __eq__, they must return the same value from __hash__. This invariant is what allows sets and dictionaries to locate objects efficiently. When you insert an object into a set, Python computes its hash to find the bucket, then uses __eq__ to resolve collisions. If equal objects had different hashes, they would be placed in different buckets, and membership checks would fail.
Python enforces this contract implicitly when you use built-in types, but for custom classes you are responsible for maintaining it. The language provides a default __hash__ that is based on id(), which is consistent with the default __eq__ that also uses identity. As soon as you override __eq__, Python assumes you are changing equality semantics and disables the default hash by setting __hash__ = None unless you explicitly define it.
What Happens When You Define Only __eq__
Consider a class that defines __eq__ but not __hash__:
class Point: def __init__(self, x, y): self.x = x self.y = y def __eq__(self, other): return isinstance(other, Point) and (self.x, self.y) == (other.x, other.y)
Instances of this class are unhashable:
p1 = Point(1, 2) p2 = Point(1, 2) print(p1 == p2) # True print(hash(p1)) # TypeError: unhashable type: 'Point'
This behavior is intentional. If Python kept the default id()-based hash, two equal points would have different hashes, violating the contract. By making the object unhashable, Python forces you to decide whether you want value-based equality and, if so, to provide a matching hash.
Implementing Both Methods Correctly
To make a class hashable with value-based equality, define both __eq__ and __hash__. The hash should be derived from the same attributes used in equality. A common pattern is to hash a tuple of those 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, self.y) == (other.x, other.y) def __hash__(self): return hash((self.x, self.y))
Now p1 and p2 have the same hash and compare equal, so they can be used in sets and as dictionary keys:
s = {p1, p2} print(len(s)) # 1
The tuple (self.x, self.y) is immutable, and hash() of a tuple combines the hashes of its elements in a deterministic way. This approach works as long as the attributes themselves are hashable.
The Role of Immutability
A hashable object must not change its hash value after it has been inserted into a set or dictionary. If an object's attributes are mutable, and those attributes contribute to the hash, then modifying the object changes its hash, breaking the data structure's internal invariants. For example:
class MutablePoint: def __init__(self, x, y): self.x = x self.y = y def __eq__(self, other): return isinstance(other, MutablePoint) and (self.x, self.y) == (other.x, other.y) def __hash__(self): return hash((self.x, self.y)) p = MutablePoint(1, 2) s = {p} p.x = 3 print(p in s) # False, even though p is still in the set
After mutating p.x, its hash changes, so the set no longer finds it in the original bucket. This is why hashable objects are typically immutable. If you need mutable objects with value equality, consider using a frozen dataclass or making attributes private and only exposing read-only properties.
Common Mistakes That Break Hashing
A frequent error is using a mutable attribute in the hash function, such as a list or a dict. For instance:
class BadHash: def __init__(self, items): self.items = items # list is mutable def __eq__(self, other): return isinstance(other, BadHash) and self.items == other.items def __hash__(self): return hash(tuple(self.items))
If self.items changes after the object is inserted into a set, the hash changes, leading to the same problem as above. Even if you don't mutate the object, using a list as part of the hash tuple raises TypeError because hash(tuple(self.items)) works only if the list is converted to a tuple at hash time, but the list itself is not hashable. The correct approach is to store an immutable tuple from the start.
Another mistake is defining __eq__ that compares against objects of a different type, but then including the type in the hash. For example, comparing a Point to a tuple might be convenient, but if the hash is based on the tuple of coordinates, two different types with the same coordinates would have the same hash but not compare equal. This is allowed but can lead to surprising behavior when mixing types in sets.
Performance Impact of a Poor Hash Function
The quality of your __hash__ implementation directly affects the performance of sets and dictionaries. A hash function that returns the same constant for all objects, such as return 0, is valid but causes every object to land in the same bucket. This turns set lookups from O(1) into O(n) in the worst case, because every insertion and lookup must compare against all existing objects via __eq__. While the contract is satisfied, the performance degrades significantly for large collections.
A good hash function should distribute objects uniformly across the hash space. Using hash() on a tuple of immutable attributes usually achieves this because Python's tuple hash combines element hashes with a well-tested algorithm. Avoid inventing your own hash arithmetic unless you have a specific reason; the built-in hash() is designed to be fast and well-distributed.
Note that Python's hash() for strings and numbers is randomized per process for security reasons, but the relative ordering of hashes remains consistent within a single run. This does not affect the contract as long as you rely on hash() rather than hardcoding numeric values.
Using dataclass and NamedTuple for Automatic Hashing
Python's dataclasses module can generate __eq__ and __hash__ for you. By default, a dataclass with eq=True (the default) sets __hash__ to None, making instances unhashable. To get a hash, you must set frozen=True, which makes the dataclass immutable and generates a hash based on the fields:
from dataclasses import dataclass @dataclass(frozen=True) class Point: x: int y: int
Now Point instances are hashable and compare by value. The generated __hash__ combines the hashes of the fields in a deterministic order.
Similarly, NamedTuple subclasses are immutable and hashable by default, with equality based on field values. If you need a lightweight data container, a NamedTuple is often simpler than a custom class.
When to Deliberately Set __hash__ = None
There are cases where you want value equality but do not want objects to be usable as dictionary keys or set members. For example, a mutable object that is compared by value but should not be hashed because its state changes frequently. Setting __hash__ = None explicitly makes the object unhashable while keeping __eq__:
class MutableRecord: def __init__(self, value): self.value = value def __eq__(self, other): return isinstance(other, MutableRecord) and self.value == other.value __hash__ = None
This is the same behavior Python applies automatically when you define __eq__ without __hash__. Being explicit can improve readability, especially when the class is part of a public API. You might also set __hash__ = None to prevent accidental use in sets when you know the object's equality is not stable over time.
In summary, the decision hinges on whether your objects are immutable and whether you need them in hash-based collections. If you need value equality and hashability, implement both methods consistently or use frozen=True dataclasses. If mutability is required, keep the object unhashable and rely on other data structures like lists for membership checks.