Understanding the Python Hash Equality Contract
python hash equality contract: Learn how Python's hash and equality methods must stay consistent, why violations break dict and set behavior, and how to implement them...
In Python, the python hash equality contract is the rule that objects which compare equal must have the same hash value. This rule is not optional: violating it breaks dictionaries, sets, and any other hash-based collection. These collections use the hash to find a bucket, then use equality to confirm the match. If two equal objects produce different hashes, the lookup can land in the wrong bucket and the object becomes unreachable.
What the Hash/Equality Contract Requires
The contract is simple: if a == b is True, then hash(a) == hash(b) must also be True. The reverse is not required; two unequal objects may share a hash, which is a collision. Python's dict and set rely on this invariant. When you insert an object as a key, Python stores its hash. When you look up a key, Python computes the hash of the probe object, jumps to the corresponding bucket, and then uses __eq__ to compare the probe with any stored object in that bucket.
If the contract is violated, the lookup may never reach the stored object because the probe's hash points to a different bucket. The stored object remains in the collection, but you cannot retrieve it. This is a silent and confusing bug.
Why Consistent Hash and Equality Matters in dict and set
Consider a dictionary keyed by a custom class. The __hash__ method determines the bucket, and __eq__ determines the final match. If two keys are equal but have different hashes, the dictionary may treat them as separate entries. Worse, if you mutate a key after insertion, its hash changes, and the dictionary still uses the old hash internally. The key becomes orphaned: it is still present, but lookup by the same object now computes a different hash and fails.
Sets behave the same way. Membership tests, union, intersection, and difference all depend on the contract. A set that contains an object whose hash changes after insertion can no longer find that object. The object is effectively lost from the set, even though it remains in memory.
Implementing hash and eq Correctly
For an immutable class, implementation is straightforward. Define __eq__ based on a tuple of the fields that define identity, and define __hash__ by hashing that same tuple. Here is a minimal example:
class Point: def __init__(self, x, y): self.x = x self.y = y def __eq__(self, other): if not isinstance(other, Point): return NotImplemented return (self.x, self.y) == (other.x, other.y) def __hash__(self): return hash((self.x, self.y))
Using the same tuple for both methods guarantees that equal objects produce the same hash. The isinstance check prevents accidental equality with unrelated types. Returning NotImplemented for incompatible types lets Python fall back to the other operand's __eq__ or to the default identity comparison.
If you only need equality and have no reason to customize hashing, you can use @dataclass(frozen=True) or @dataclass(eq=True) with unsafe_hash=False. The frozen dataclass generates both __hash__ and __eq__ from the fields, preserving the contract automatically.
The Consequences of Violating the Contract
A common mistake is defining __eq__ without redefining __hash__. In Python, if a class defines __eq__, its __hash__ is set to None unless explicitly redefined. This makes instances unhashable, so they cannot be used as dict keys or set members. The error appears immediately: TypeError: unhashable type: 'Point'. This is a clear signal, but some developers try to work around it by assigning a constant hash, which is legal but often a bad idea.
A more subtle violation occurs when a class is hashable but its __hash__ does not align with __eq__. For example, if __hash__ uses only one field while __eq__ compares two, two objects that are equal may have different hashes. This violates the contract and produces the silent lookup failure described earlier.
Another violation happens when an object is mutable and its hash depends on mutable state. Even if __hash__ and __eq__ are consistent at creation, changing a field after the object is placed in a dict or set breaks the stored hash. The object is no longer findable by its own current value.
Common Pitfalls and How to Avoid Them
One pitfall is using a mutable field in __hash__. Lists, dictionaries, and sets are unhashable, so Python raises an error if you try to hash them. But a custom class can have a mutable attribute that is not directly hashable, such as a list used in __eq__. If you convert that list to a tuple for hashing, the hash changes when the list changes. The object becomes dangerous as a key.
Another pitfall is forgetting to update __hash__ when you override __eq__. Python's default behavior is to set __hash__ to None in that case. If you want to keep the object hashable, you must explicitly define __hash__. The safest pattern is to make the class immutable: set all attributes in __init__ and prevent later assignment. You can use @dataclass(frozen=True) or manually override __setattr__ to raise an error.
A third pitfall is relying on identity for equality while using a custom hash. If __eq__ is the default object identity, then two distinct objects are never equal, so any hash is technically consistent. But if you override __hash__ without overriding __eq__, you create a class where equal objects (by identity) share a hash, but unequal objects may also share it. That is allowed, but it can lead to many collisions and poor performance.
Performance and Operational Considerations
Hash distribution directly affects the performance of dict and set operations. If many objects share the same hash, they land in the same bucket, and lookup degrades from O(1) to O(n) in the worst case. A well-designed __hash__ spreads values across the hash space. Using hash() on a tuple of the relevant fields is usually sufficient for small, immutable objects. For more complex objects, consider combining field hashes with XOR or a deterministic mixing function, but avoid inventing a custom hash function unless you have measured a real collision problem.
The contract also has operational implications. When you serialize objects with pickle or JSON, the hash is not stored; it is recomputed on deserialization. If the deserialized object has different field values, its hash changes, and it will not match a key that was stored before serialization. This is another reason to keep hashable objects immutable and to base __hash__ only on fields that define the object's identity.
Designing Hashable Types for Maintainability
The easiest way to maintain the contract is to keep hashable types immutable. Use @dataclass(frozen=True) for simple value objects. If you need custom behavior, follow the rule: __eq__ and __hash__ must be based on the same set of attributes. Document that the object is immutable and that changing any attribute after creation is unsupported.
For classes that must be mutable, do not make them hashable. Leave __hash__ as None and use them only as values, not as keys. If you need a mutable object to be a key, consider using an immutable wrapper or a unique identifier field that never changes.
When a class inherits from a base that defines __hash__ and __eq__, be careful about adding new fields. The subclass's __eq__ may compare the new fields, but the inherited __hash__ does not include them, violating the contract. Override both methods in the subclass to include the new fields.
Finally, test the contract explicitly. Write unit tests that create two equal objects and assert that their hashes are equal. Also test that inserting an object into a dict and looking it up with an equal object returns the expected value. This catches contract violations early, before they cause production bugs.
Edge Cases and Advanced Usage
Python's hash() function returns an integer, but for user-defined classes it may return any integer, including negative values. The hash of a tuple is computed from the hashes of its elements, so a tuple containing a mutable object is unhashable. This is a useful property: it prevents you from accidentally creating a key that can change.
If you need a hash that is consistent across processes or Python versions, you must implement it yourself using a stable algorithm. The built-in hash() is randomized for strings and bytes in each process to prevent denial-of-service attacks on dicts. Do not rely on it for persistence or cross-process communication.
Another edge case is the interaction with functools.total_ordering. That decorator only generates comparison methods, not __hash__. If you use it, you still need to define __hash__ explicitly or rely on the default identity hash. The contract still applies.
When using dataclasses, the eq and frozen parameters control hash generation. A non-frozen dataclass with eq=True gets __hash__ = None. A frozen dataclass gets a hash based on its fields. If you set unsafe_hash=True, Python generates a hash even for non-frozen dataclasses, but you must ensure the fields used for hashing are never mutated. The name unsafe_hash is a warning: it is safe only if you treat the object as immutable.
In performance-sensitive code, you can cache the hash value on first computation. This is useful for objects that are expensive to hash and are used repeatedly as keys. However, caching only works if the object is truly immutable; otherwise the cached hash becomes stale. If you implement caching, store the hash in a private attribute and compute it lazily. This adds complexity but can reduce overhead in long-running processes.
A final consideration is the use of __hash__ with NotImplemented. If __eq__ returns NotImplemented for an incompatible type, Python may fall back to the other operand's __eq__. The hash contract only applies when equality is True. If two objects of different types are never equal, they do not need to share a hash. This is why returning NotImplemented is the correct behavior for type mismatches.
By following these rules, you keep the python hash equality contract intact, and your dicts and sets behave predictably. The contract is not just a theoretical guideline; it is a hard requirement that Python's collections enforce at runtime. Violations are subtle and can cause data loss in production. Design your hashable classes with immutability and consistency in mind, and test the contract explicitly to avoid the most confusing bugs in Python development.