Day 12 · ~15m

Python Special Methods: __str__, __repr__, and __eq__ Explained

Make your objects behave like native Python types. Learn dunder methods: __str__ for human-readable output, __repr__ for debugging, __eq__ for comparison, __len__ for collection behavior.

student (struggling)

Look, I've been debugging Order objects since yesterday and every time I print one it's just <Order object at 0x10a8e5410>. I have no idea what's in it. Amir's Order class shows the actual data when you print it.

teacher (neutral)

Show me your Order class.

student (neutral)

Nothing fancy:

teacher (neutral)
class Order:
    def __init__(self, order_id, customer, items, total):
        self.order_id = order_id
        self.customer = customer
        self.items = items
        self.total = total

order = Order("ORD-001", "Alice Chen", 3, 124.50)
print(order)  # <Order object at 0x10a8e5410>
teacher (focused)

That memory address is Python's default. When you print() an object, Python looks for a method called __str__. If it does not exist, it gives you the address instead.

student (curious)

So I need to add a method to make it print something useful?

teacher (serious)

Yes. The dunder methods — that is, the double-underscore methods — are Python's protocol. When you implement them, your objects start to behave like builtin types. Let me show you:

class Order:
    def __init__(self, order_id, customer, items, total):
        self.order_id = order_id
        self.customer = customer
        self.items = items
        self.total = total

    def __str__(self):
        return f"Order {self.order_id}: {self.customer} — {self.items} items, ${self.total:.2f}"

order = Order("ORD-001", "Alice Chen", 3, 124.50)
print(order)  # Order ORD-001: Alice Chen — 3 items, $124.50
student (excited)

That's it? Just add a method that returns a string?

teacher (encouraging)

Exactly. __str__ is called whenever you print() the object or call str() on it. It is the human-readable version.

student (thinking)

Okay. But Amir's code has more than just __str__. When he prints an Order in the debugger, it shows different output.

teacher (focused)

He probably has __repr__ too. That is the developer-readable version — what you see in the debugger or the REPL.

class Order:
    def __init__(self, order_id, customer, items, total):
        self.order_id = order_id
        self.customer = customer
        self.items = items
        self.total = total

    def __str__(self):
        return f"Order {self.order_id}: {self.customer} — {self.items} items, ${self.total:.2f}"

    def __repr__(self):
        return f"Order(order_id={self.order_id!r}, customer={self.customer!r}, items={self.items}, total={self.total})"

order = Order("ORD-001", "Alice Chen", 3, 124.50)
print(order)     # Order ORD-001: Alice Chen — 3 items, $124.50
print(repr(order))  # Order(order_id='ORD-001', customer='Alice Chen', items=3, total=124.5)
student (confused)

What is the difference? Both print something about the order.

teacher (serious)

__str__ is for the end user. __repr__ is for the developer. Think of it this way: __str__ is what the customer sees on their receipt. __repr__ is what you see in the source code or the debugger when you need to understand what the object actually contains.

student (thinking)

So if I have two orders with the same ID, are they the same order?

teacher (neutral)

Python would say no.

order1 = Order("ORD-001", "Alice Chen", 3, 124.50)
order2 = Order("ORD-001", "Alice Chen", 3, 124.50)

print(order1 == order2)  # False — different objects in memory
print(order1 is order2)  # False
student (surprised)

But they are literally the same order!

teacher (encouraging)

Exactly. Which is why you need __eq__. That method tells Python how to compare two instances of your class.

class Order:
    def __init__(self, order_id, customer, items, total):
        self.order_id = order_id
        self.customer = customer
        self.items = items
        self.total = total

    def __str__(self):
        return f"Order {self.order_id}: {self.customer} — {self.items} items, ${self.total:.2f}"

    def __repr__(self):
        return f"Order(order_id={self.order_id!r}, customer={self.customer!r}, items={self.items}, total={self.total})"

    def __eq__(self, other):
        if not isinstance(other, Order):
            return False
        return self.order_id == other.order_id

order1 = Order("ORD-001", "Alice Chen", 3, 124.50)
order2 = Order("ORD-001", "Alice Chen", 3, 124.50)
order3 = Order("ORD-002", "Bob Kumar", 1, 89.00)

print(order1 == order2)  # True — same order ID
print(order1 == order3)  # False — different order ID
student (thinking)

So you check isinstance first? What if someone compares an Order to a string or something?

teacher (focused)

That is the first thing you do in __eq__. If the other object is not an Order, return False. Python will handle the comparison gracefully.

student (curious)

Can you do this for other operations? Like, what if I want to compare orders by total?

teacher (serious)

You can. That is a different dunder method — __lt__ for less than, __gt__ for greater than. But let us stay with __eq__ for now. It is the most common.

student (focused)

Okay. You said there was one more method?

teacher (neutral)

__len__. If your order has multiple items, you might want len(order) to return the number of items.

class Order:
    def __init__(self, order_id, customer, items, total):
        self.order_id = order_id
        self.customer = customer
        self.items = items
        self.total = total

    def __str__(self):
        return f"Order {self.order_id}: {self.customer} — {self.items} items, ${self.total:.2f}"

    def __repr__(self):
        return f"Order(order_id={self.order_id!r}, customer={self.customer!r}, items={self.items}, total={self.total})"

    def __eq__(self, other):
        if not isinstance(other, Order):
            return False
        return self.order_id == other.order_id

    def __len__(self):
        return self.items

order = Order("ORD-001", "Alice Chen", 3, 124.50)
print(len(order))  # 3
student (excited)

So now Order behaves like a list? You can print it, compare it, and check its length?

teacher (proud)

You just described the entire purpose of dunder methods. You make your custom objects feel like the builtin types your users already understand.

student (thinking)

This is so much code. Do I have to add all four methods every time I make a class?

teacher (neutral)

Not every time. Add __str__ and __repr__ when you want debugging to be easy. Add __eq__ when two instances with the same data should be considered equal. Add __len__ when your object represents a collection. For your Order class, you probably want all of them.

student (focused)

Alright. I am going to add all four to my Order class. Test it out. Then what?

teacher (serious)

Then you have a Product class, which has the exact same structure as Order. Product, customer, count, price. You are going to want to add the same four methods there. And that is when you learn about inheritance — which is just saying "don't copy-paste. Inherit from a shared parent class."

student (amused)

Tomorrow's lesson, I assume.

teacher (amused)

Would be a shame to teach you the solution before the problem.

student (proud)

I can already feel myself getting better at this.