Dunder methods

What 'dunder' means, and why print() doesn't already work

You’ve been writing __init__ since the start of this lesson without a formal explanation of the naming — dunder is short for “double underscore,” and Python reserves this naming pattern (__name__) for a set of special methods it calls automatically in specific situations, rather than ones you call directly by name. __init__ is one dunder method among many; this section covers the ones that matter most day to day.

A fresh class, with no dunders defined beyond __init__, prints something not very useful:

Try it — edit and run

That’s Python’s default object representation — the class name and a memory address, not remotely useful for debugging or logging. Dunder methods are how you tell Python what your own objects should do in situations like this one.

__str__ — what print() and str() show

Defining __str__ controls what print(agent) and str(agent) display, replacing that default representation with something meaningful:

Try it — edit and run

You never call agent.__str__() directly — print() and str() call it for you, automatically, the moment they receive an object of this class. This is the core pattern behind every dunder method: you define the method, but something else in Python triggers it, at the right moment, without you invoking it by name.

__repr__ — the developer-facing version

__repr__ is a second, related method with a different audience: __str__ is meant for a human reading output (like a print() in a UI or a log line), while __repr__ is meant for a developer debugging — ideally unambiguous enough that, in principle, eval(repr(obj)) could recreate an equal object. It’s what shows up in a REPL when you type a variable name with no print(), and it’s what you get when __str__ isn’t defined:

Try it — edit and run

!r inside the f-string forces repr() on that specific value rather than str() — worth using here since it makes self.name show as 'research_agent' with quotes, matching how you’d actually type a string literal in code, which is the whole point of __repr__’s “could recreate this” spirit.

If you define only one of the two, define __repr__ — it’s the fallback Python reaches for automatically if __str__ is missing (as the earlier default <__main__.Agent object at 0x...> output actually was — that’s object’s own default __repr__, which every class inherits unless overridden). Defining __str__ alone leaves debugging contexts (a REPL, a list of objects, a debugger) stuck with the unhelpful default.

__eq__ — what == means for your objects

By default, == between two instances checks whether they’re the same object in memory — identical to Java’s default .equals() behavior before it’s overridden, or JS’s === on objects:

Try it — edit and run

Even though agent_a and agent_b hold identical data, == says they’re unequal — by default it’s really asking “are these the exact same object,” not “do these hold the same values.” Defining __eq__ lets you specify what equality should actually mean for this class:

Try it — edit and run

__eq__ takes self and otherother is whatever’s on the right side of ==, and you decide what makes the two equal. agent_a == agent_b is really Python calling agent_a.__eq__(agent_b) behind the scenes, following the same automatic-trigger pattern as __str__ and __repr__.

Operators are just dunder methods in disguise

__eq__ generalizes to something worth naming explicitly: most Python operators you already use every day — ==, +, <, in, and more — are themselves just dunder method calls, dispatched automatically based on the operator used. a + b is a.__add__(b); a < b is a.__lt__(b). This lesson only covers __eq__ in depth (the one that matters most for everyday agent code — comparing two tool calls, two config objects), but the pattern is general: any operator’s behavior on your own class can be customized by defining the matching dunder.

Try it — edit and run

You’d never actually write .__eq__(...) directly in real code — this is only to make the underlying mechanism visible once, the same way greet = shout(greet) made the decorator mechanism visible in the decorator concept earlier in this lesson before switching to the @ syntax you’d actually use.

Check your understanding
1/6

What does Python print for an object with no __str__ or __repr__ defined?

Exercise · Graded

Implement __init__ to store tool_name and arguments, __eq__ to compare both fields between two ToolCall instances, and __repr__ to return a string in the exact format shown, using !r so both values are formatted the way they'd appear as Python literals.