C.W.K.
Stream
Lesson 03 of 07 · published

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)
With dunders — the language plays along·python
class Point:
    def __init__(self, x, y):
        self.x, self.y = x, y

    def __repr__(self):
        return f"Point(x={self.x}, y={self.y})"

    def __str__(self):
        return f"({self.x}, {self.y})"

    def __eq__(self, other):
        if not isinstance(other, Point):
            return NotImplemented
        return self.x == other.x and self.y == other.y

    def __hash__(self):
        return hash((self.x, self.y))

p = Point(3, 4)
p2 = Point(3, 4)
p3 = Point(5, 6)

print(repr(p))          # 'Point(x=3, y=4)'
print(str(p))           # '(3, 4)'
print(p == p2)          # True
print(p == p3)          # False
print({p, p2, p3})      # {Point(x=3, y=4), Point(x=5, y=6)}    — dedup'd
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'

External links

Exercise

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.
Spotted a bug or have feedback on this page?Report an Issue

Comments 0

🔔 Reply notifications (sign in)
Sign inPlease sign in to comment.

No comments yet — be the first.