Learn by reading through in order

Special Methods — Teaching Your Class How + and print Behave

Learn Python's special methods (dunder methods). Customize + with __add__, control print output with __str__, and redefine == with __eq__ — all with diagrams.

Last time we sorted out instance, class, and static methods. Now we'll cover a different kind — special methods (a.k.a. dunder methods).

What Are Special Methods? — "Dunder Methods"

Methods like __init__ and __str__ — names wrapped in two underscores on each side — are called special methods. Because they sit between double underscores, they're also called dunder methods.

The key thing about special methods is that you don't call them directly. Python calls them automatically when you perform certain operations. Write v1 + v2 and Python calls v1.__add__(v2) under the hood. Write print(p) and p.__str__() is called. Write a == b and a.__eq__(b) runs.

Operators and Their Special Methods
What youwritePythonconvertsMethodcalledv1 + v2__add__print(p)__str__a == b__eq__
Python's operators and built-in functions call matching dunder methods behind the scenes. Define them on your class and your own type can use +, ==, and friends.

__add__ — Defining the + Operator

Take + as the obvious example. Suppose you have a Money class for amounts in yen, and you want Money(300) + Money(500) to give you Money(800). Try to add them out of the box and Python will throw a TypeError complaining it doesn't know how to add Money to Money.

The way to teach Python how to add them is the __add__ method. Define def __add__(self, other): and return a new instance built from self (the left side) and other (the right side). Now + works on your class.

class Money:
    def __init__(self, amount):
        self.amount = amount

    def __add__(self, other):                 # called when you write +
        return Money(self.amount + other.amount)

wallet  = Money(300)
payment = Money(500)
total   = wallet + payment                    # really wallet.__add__(payment)
print(total.amount)                           # 800
What v1 + v2 Does Under the Hood
wallet+ paymentwallet.__add__(payment)self =walletother =paymentreturn Money(self.amount + other.amount)returnsMoney(800)translates
Write + and Python calls self.__add__(other). self is the left operand, other is the right one. Whatever new instance you return becomes the result of +.

Implement __add__ on Money so + adds two balances together.

① Define class Money: with __init__(self, amount) that assigns self.amount = amount.

② Define __add__(self, other) that returns Money(self.amount + other.amount).

③ Build wallet = Money(300) and payment = Money(500), then print total.amount where total = wallet + payment.

(Once it runs correctly, the explanation will appear.)

Python Editor

Run code to see output

__str__ and __repr__ — Two Kinds of String Representation

Next up: the special methods called when an instance gets converted to a string. There are two of them, with different goals.

- __str__ — called by print(p) and str(p). A user-friendly, readable string.

- __repr__ — called in the REPL or for debugging. A code-like, detailed string.

With no overrides, print(user) shows something like <__main__.User object at 0x...> — pretty useless. Define __str__ for a clean display, and define __repr__ too so debugging shows you the type and the contents at a glance.

class User:
    def __init__(self, name, age):
        self.name = name
        self.age  = age

    def __str__(self):                       # for print() / str()
        return f"{self.name} ({self.age} years old)"

    def __repr__(self):                      # for debugging
        return f"User(name={self.name!r}, age={self.age})"

u = User("Alice", 30)
print(u)             # Alice (30 years old)         <- __str__
print(repr(u))       # User(name='Alice', age=30)   <- __repr__
When to Use __str__ vs __repr__
__str__ (user-facing)
  • Called by print(u) and str(u)
  • Goal: a string for end users to read
  • Example: Alice (30 years old)
__repr__ (developer-facing)
  • Called by repr(u) and the interactive REPL
  • Falls back to here when __str__ is missing during print
  • Goal: a debug string showing type and contents
  • Example: User(name='Alice', age=30)
Defining both is ideal, but if you only define one, make it __repr__ — it makes debugging much easier.

Implement __str__ and __repr__ on User and compare what print and repr show.

① Define class User: with __init__(self, name, age) that assigns self.name / self.age.

② Define __str__(self) returning f"{self.name} ({self.age} years old)".

③ Define __repr__(self) returning f"User(name={self.name!r}, age={self.age})". The !r after self.name means embed the result of calling repr() on the value — strings come out with single quotes ('Alice') instead of bare text (Alice).

④ Build u = User("Alice", 30), then run both print(u) and print(repr(u)) to see the difference.

Python Editor

Run code to see output

__eq__ — Defining ==

Now equality. When you write a == b, Python calls a.__eq__(b) internally. If your class doesn't define __eq__, the default is "are they the same object in memory?" (an id comparison).

Say you have a Coupon class and you want two coupons with the same code to count as the same coupon — even if the discount values differ. That's a "business-level" equality, and __eq__ is where you spell it out.

How __eq__ Changes the Behavior of ==
c1 == c2no__eq__id check(memory addr)with__eq__self.code== other.codedefaultdefined
Without __eq__, == is an id check — same contents but separate instances still come out False. With __eq__, you can compare by content (code).
class Coupon:
    def __init__(self, code, discount):
        self.code     = code
        self.discount = discount

    def __eq__(self, other):
        return self.code == other.code        # same code = same coupon

c1 = Coupon("SPRING10", 0.10)
c2 = Coupon("SPRING10", 0.20)               # different discount, same code
c3 = Coupon("SUMMER15", 0.15)

print(c1 == c2)   # True  (codes match)
print(c1 == c3)   # False (codes differ)

What Happens Without __eq__?

If you don't define __eq__, c1 == c2 checks whether they're the same object in memory (literally pointing to the same box). Even if every attribute matches perfectly, two separately constructed instances live at different memory addresses and the result is False. Whenever you want "equal by contents," define __eq__ yourself.

Implement __eq__ on Coupon so two coupons are equal when their code matches.

① Define class Coupon: with __init__(self, code, discount) that assigns self.code and self.discount.

② Define __eq__(self, other) that returns self.code == other.code.

③ Build three coupons — Coupon("SPRING10", 0.10), Coupon("SPRING10", 0.20), and Coupon("SUMMER15", 0.15) — and print two == comparisons.

Python Editor

Run code to see output

Other Common Special Methods — __call__ / __len__ / __bool__

Beyond the four we've covered, several other special methods come up often. Three worth knowing right away:

- __call__ — makes an instance callable like a function (obj(...) syntax)

- __len__ — defines what len(obj) returns

- __bool__ — defines how the instance evaluates as a truth value in if obj: or bool(obj)

Each one teaches a built-in operation — function call, len(), the if truthiness check — to your custom class. A Logger that accumulates log entries is a great example to put all three on one class.

class Logger:
    def __init__(self, name):
        self.name = name
        self.log  = []

    def __call__(self, message):                    # logger("...") works
        self.log.append(message)
        return f"[{self.name}] {message}"

    def __len__(self):                               # for len(logger)
        return len(self.log)

    def __bool__(self):                              # for if logger:
        return len(self.log) > 0

app = Logger("app")
print(app("App started"))    # [app] App started
print(app("Login successful"))   # [app] Login successful
print(len(app))               # 2
if app:
    print("There are logs")    # There are logs
Teaching Logger to Speak Built-in Syntax
app('msg')app.__call__('msg')len(app)app.__len__()if app:app.__bool__()becomesbecomesbecomes
Familiar syntax — logger(...), len(logger), if logger: — is taught to your custom class through three matching dunders.

What If You Don't Define __bool__?

When __bool__ is missing, Python falls back to __len__. A length of 0 becomes False; anything else becomes True. If both are missing, instances are always truthy, no matter their state. That's the same rule that makes empty lists and empty strings evaluate to False.

Implement the three special methods on Logger so adding messages, counting them, and checking emptiness all work with built-in syntax.

① Define class Logger: with __init__(self, name) setting self.name = name and self.log = [].

② Define __call__(self, message): append message to self.log, then return f"[{self.name}] {message}".

③ Define __len__(self) returning len(self.log).

④ Define __bool__(self) returning len(self.log) > 0.

⑤ Build error_log = Logger("error"), then run in order: print(bool(error_log))error_log("DB error")print(len(error_log))print(bool(error_log)).

Python Editor

Run code to see output

There are plenty more — __hash__ (for use as a set/dict key), __lt__ / __gt__ (the < / > comparisons), __getitem__ (the obj[key] syntax), __iter__ (for x in obj: iteration), and others. You don't need to memorize them all. Just keep this mental map: "if you want to teach a built-in operation to your class, there's probably a matching dunder for it." Then look it up when you need it.

Special methodSyntaxWhen it's called
__init__Money(300)On instance creation
__add__a + b+ operator
__str__print(p) / str(p)User-facing string
__repr__repr(p) / REPL displayDeveloper-facing string
__eq__a == bEquality comparison
__call__obj(...)Function-style call
__len__len(obj)Length
__bool__if obj: / bool(obj)Truthiness
QUIZ

Knowledge Check

Answer each question one by one.

Q1When result = a + b runs, which method does Python call behind the scenes?

Q2Which description of __str__ and __repr__ is most accurate?

Q3If a class doesn't define __eq__, what does a == b compare? (Both are instances of the same class.)