← Python Code Modern OOP
Browse Python Concepts

Dunder Methods That Actually Matter

Mental Model

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.

Rule: Always implement __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?

Broken code
Python
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}
Predict what happens when you run this code. Does it create a set with one element, two elements, or raise an error?

The Output

What actually happens
TypeError: unhashable type: 'APIClient'

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

Corrected pattern
Python
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

Always implement __hash__ if you override __eq__ on a class whose instances must be stored in sets or used as keys in dictionaries.
Common mistake: Developers override __eq__ for custom equality checks but forget that Python automatically sets __hash__ = None when __eq__ is defined without __hash__, making instances unhashable.