Dunder Methods That Actually Matter
Imagine __eq__ as determining if two people are identical twins. __hash__ is like their unique ID card. If you say they are identical twins (__eq__), Python removes their original ID cards and expects you to issue new ones (__hash__), otherwise they can't be organized in a system that relies on unique IDs.
__hash__ if you override __eq__ on a class whose instances must be stored in sets or used as keys in dictionaries.The Setup
You are writing an API identity manager. To avoid duplicate tracking of the same logical client instance, you override the equality method __eq__ based on a unique database ID. Later, you attempt to deduplicate these clients by throwing them into a set.
What Does This Print?
class APIClient:
def __init__(self, client_id: str):
self.client_id = client_id
def __eq__(self, other):
if not isinstance(other, APIClient):
return NotImplemented
return self.client_id == other.client_id
client_1 = APIClient("auth-service")
client_2 = APIClient("auth-service")
# Deduplicate using a set
unique_clients = {client_1, client_2}
The Output
Python throws a runtime TypeError: unhashable type: 'APIClient' when trying to construct the set. By defining a custom __eq__ method, Python automatically overrides the default implementation of __hash__ by setting it to None. This prevents your custom object from being added to sets or utilized as a key in dictionaries.
Why Python Does This
In Python, the hash value of an object must remain constant across its entire lifecycle to guarantee dictionary and set lookups function in $O(1)$ time. If two objects are equal (a == b), they must have the same hash value (hash(a) == hash(b)). When you write a custom __eq__, Python can no longer guarantee that the default object-identity-based hash (derived from the memory address) satisfies this condition. To prevent quiet hash collisions and corrupted lookup keys in hash maps, the interpreter implicitly sets __hash__ = None on class definition.
The Fix
class APIClient:
def __init__(self, client_id: str):
self.client_id = client_id
def __eq__(self, other):
if not isinstance(other, APIClient):
return NotImplemented
return self.client_id == other.client_id
def __hash__(self):
# Must hash the identical attributes evaluated in __eq__
return hash(self.client_id)
client_1 = APIClient("auth-service")
client_2 = APIClient("auth-service")
unique_clients = {client_1, client_2} # Works perfectly now!
Implementing a custom __hash__ method provides a consistent integer hash value for instances. Python needs this hash to efficiently determine the correct bucket in a hash-based data structure (like a set or dictionary) before performing a full __eq__ comparison.
How This Fails in Real Systems
A high-performance trading adapter group implemented custom __eq__ for currency instruments but forgot to define __hash__. A downstream component stored connections in dictionary lookups. A minor code change triggered a fallback block, throwing unhandled unhashable-type exceptions during active trading hours, delaying pricing updates for 8 minutes.
Key Takeaway
__hash__ if you override __eq__ on a class whose instances must be stored in sets or used as keys in dictionaries.__eq__ for custom equality checks but forget that Python automatically sets __hash__ = None when __eq__ is defined without __hash__, making instances unhashable.