Dunder Methods Part 1 — __repr__, __str__, __eq__, __hash__
~22 min · dunder, repr, eq, hash
Level 0Curious
0 XP0/93 lessons0/23 achievements
0/100 XP to next level100 XP to go0% complete
The methods that integrate your class with the language
Dunder methods (double-underscore methods) are how Python's built-in operations talk to your classes. print(obj) calls obj.__str__. obj1 == obj2 calls __eq__. {obj: 1} calls __hash__. Implementing the right dunders makes your class behave like a native Python type.
__repr__ vs __str__ — the unambiguous and the friendly
__repr__ should be unambiguous — ideally a string that, when evaluated, recreates the object: Point(x=3, y=4). It's what you see in the REPL and in debugger output. __str__ is the friendly, user-facing string — used by print and str(obj). If you only define one, define __repr__ — Python falls back to it if __str__ is missing.
__eq__ — value equality
By default, obj1 == obj2 is identity (obj1 is obj2). Override __eq__ to compare by value — typically by comparing relevant attributes. Two Point(3, 4) instances should be equal even if they're different objects.
__hash__ — required to be a dict key or set member
If you override __eq__, Python sets __hash__ to None, which means your instances can't be hashed. Reason: Python's contract says equal objects must have equal hashes. If you override one, you must keep them consistent. The fix: define __hash__ alongside __eq__, typically returning hash((self.attr1, self.attr2)). For mutable objects, the right answer is often to leave __hash__ as None — mutable objects shouldn't be hashed.
Principle: Three dunders form a coherent set: __repr__ for debugging, __eq__ for value comparison, __hash__ for collection membership. Define them together, in that order, and your class slots into the language nicely. Or use @dataclass and have them auto-generated (lesson 6).
Code
Without dunders — the default behavior·python
class Point:
def __init__(self, x, y):
self.x, self.y = x, y
p = Point(3, 4)
print(p) # <__main__.Point object at 0x...>
print(repr(p)) # same
p2 = Point(3, 4)
print(p == p2) # False <- identity comparison, not value
try:
{p} # default __hash__ uses id() — works, but useless
except TypeError as e:
print(e)
NotImplemented vs False — when you can't compare·python
class Money:
def __init__(self, amount, currency):
self.amount, self.currency = amount, currency
def __eq__(self, other):
if not isinstance(other, Money):
return NotImplemented # let Python try the OTHER side
if self.currency != other.currency:
return False # same type, but not equal
return self.amount == other.amount
m1 = Money(100, "USD")
m2 = Money(100, "USD")
m3 = Money(100, "KRW")
print(m1 == m2) # True
print(m1 == m3) # False
print(m1 == "100 USD") # False — Python sees NotImplemented, tries str.__eq__
Mutable class — leave __hash__ = None·python
class Cart:
def __init__(self):
self.items = []
def __eq__(self, other):
return isinstance(other, Cart) and self.items == other.items
# Don't set __hash__ — Python auto-sets it to None when you override __eq__
# That means Cart can't go in a set or be a dict key. That's correct:
# mutable objects shouldn't be hashed.
c = Cart()
c.items.append("apple")
try:
{c}
except TypeError as e:
print(e) # unhashable type: 'Cart'
Implement a class Card(rank, suit) representing a playing card. Add __repr__ showing 'Card(rank=Q, suit=hearts)', __str__ showing 'Q♥' (use a dict to map suit name to symbol), __eq__ comparing rank and suit, and __hash__ so Card instances can live in a set. Build a small deck — five cards, with one duplicate — put it in a set, and confirm the duplicate is removed. Print each card with both repr and str.
Progress
Progress is local-only — sign in to sync across devices.