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:
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:
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:
!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:
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:
__eq__ takes self and other — other 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.
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.
What does Python print for an object with no __str__ or __repr__ defined?
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.