Overview
A hash table's lookup speed comes from using an object's hash value to jump directly to the right bucket, then using equality only to confirm the exact match within that bucket, which is why the required contract is strict and one-directional: any two objects that compare equal must produce the same hash, or a lookup for one will search an entirely different bucket than where an "equal" object actually lives and simply never find it. In Python 3, defining a custom __eq__ without also defining __hash__ doesn't quietly leave the old identity-based hash in place, Python sets __hash__ to None on that class, making instances unhashable, so using one as a dict key raises TypeError rather than silently corrupting anything. The genuinely silent version of this bug only shows up when an incompatible hash is explicitly kept or supplied anyway (an inherited or manually reassigned identity-based __hash__ alongside a value-based __eq__), in that case a == b can be true while d.get(b) returns nothing for a dict keyed by a, with nothing raised to point at the cause. This is also exactly why Python's own guidance says a class representing mutable objects with a custom __eq__ shouldn't define __hash__ at all, a hash computed from fields that can later change would strand the object in the wrong bucket the moment it mutates.
Quick Reference
| Rule | Why |
|---|---|
a == b implies hash(a) == hash(b) | Lookup uses hash to pick the bucket, equality only confirms within it |
| Unequal objects can share a hash | Collisions are expected and handled; only equal objects sharing hashes is required |
Mutable object + custom __eq__ | Should not define __hash__; mutation would strand it in the wrong bucket |
Syntax
from dataclasses import dataclass
@dataclass(frozen=True) # immutable - x and y can't change after construction
class Point:
x: int
y: int
def __hash__(self):
return hash((self.x, self.y)) # same fields used in __eq__Examples
class Bad:
def __init__(self, v): self.v = v
def __eq__(self, other): return self.v == other.v
# Defining __eq__ without __hash__ sets __hash__ to None;
# Python makes the class unhashable rather than risk a silent
# contract violation with the old identity-based hash.
a, b = Bad(42), Bad(42)
a == b # True
hash(a) # TypeError: unhashable type: 'Bad'
d = {a: "found"} # also raises TypeError: unhashable type: 'Bad'Silent lookup failures require an explicitly retained incompatible hash - the default case raises TypeError
Python 3 protects against the common case automatically: defining __eq__ without __hash__ makes the class unhashable, so hash() raises TypeError right away instead of corrupting lookups. The silent failure mode only appears if an incompatible hash is explicitly kept or supplied anyway; then dictionary and set lookups just quietly return the wrong (or no) result, one of the harder classes of bug to catch precisely because nothing raises to point at the cause.
Visual Diagram
Common Mistakes
- Overriding
__eq__based on an object's value while leaving__hash__at its identity-based default, breaking the contract silently. - Making a mutable object hashable based on fields that can change after insertion, stranding it in the wrong bucket once mutated.
- Assuming a failed dictionary lookup for an object that "looks equal" to a stored key must be an equality bug, when it's often actually a hash mismatch.
- Treating hash collisions between unequal objects as a bug; they're expected and handled by the hash table, only equal-but-differently-hashed objects are the actual problem.
Performance
- A hash table's average-case O(1) lookup depends on hashes being well-distributed; a poor hash function that clusters many unequal objects into few buckets degrades toward O(n) lookups within those buckets.
- Computing a hash from the same fields used in equality (rather than, say, a cheaper subset) keeps the contract correct at a typically negligible added computation cost.
Best Practices
- Always derive
__hash__from exactly the same fields used in__eq__, so the required contract holds by construction. - Don't define
__hash__at all for a class representing mutable objects with a custom__eq__; let it stay unhashable rather than risk silent corruption. - If a lookup for an "equal" key mysteriously fails, check the hash/equality contract first, it's a common, hard-to-spot root cause.
- Prefer immutable, value-based key types (tuples, frozen dataclasses) for dictionary keys over mutable custom objects when possible.