AcademyPython
OOP and Dunder Methods in Python
How __init__, __repr__ and __eq__ hook a class into built-in operators, why super() matters in a diamond hierarchy, and when composition beats inheritance.
3 min readUpdated 3 exercises
Dunder methods are how Python's own syntax reaches your class
len(x), x == y, print(x), x + y are not special-cased for built-in types only — each one calls a method with a double-underscore name on x, and defining that method on a custom class makes the built-in syntax work on it too:
class Point:
def __init__(self, x, y):
self.x = x
self.y = y
def __repr__(self):
return f'Point({self.x}, {self.y})'
def __eq__(self, other):
return isinstance(other, Point) and self.x == other.x and self.y == other.y
def __add__(self, other):
return Point(self.x + other.x, self.y + other.y)Point(1, 2) + Point(3, 4) now works because + calls __add__. Point(1, 2) == Point(1, 2) returns True because == calls __eq__ — without it, two Points with identical coordinates would compare unequal, since the default __eq__ inherited from object compares identity, not value.
repr versus str
__repr__ is what a debugger, a REPL, and a list's own repr() show — unambiguous, ideally valid Python that could recreate the object. __str__ is what print() and str() use for a reader-facing description, and it falls back to __repr__ automatically if not defined. Defining only __repr__ and skipping __str__ is the normal, idiomatic choice for most classes — that fallback exists precisely so one method covers both cases unless the two genuinely need to differ.
__init__ is not the constructor
__new__ creates the instance; __init__ only initialises one that already exists. __init__ returning anything other than None is a TypeError, because by the time it runs the object already exists — its job is to set attributes, not produce a value. Overriding __new__ is rare and mostly reserved for immutable types or metaclass work; almost every class only ever needs __init__.
super() and the method resolution order
class Base:
def __init__(self):
print('Base')
class Left(Base):
def __init__(self):
super().__init__()
print('Left')
class Right(Base):
def __init__(self):
super().__init__()
print('Right')
class Diamond(Left, Right):
def __init__(self):
super().__init__()
print('Diamond')
Diamond()
# Base
# Right
# Left
# DiamondBase prints once, not twice, even though both Left and Right inherit from it. super() does not mean "my direct parent" — it means "the next class in the method resolution order," a single linearised sequence Python computes once per class (Diamond.__mro__) that visits every ancestor exactly once. That is what keeps a diamond-shaped hierarchy from running a shared base class's __init__ twice, which would happen if super() were a literal reference to the immediate parent instead.
Where composition reads better
class Engine:
def start(self):
...
class Car:
def __init__(self):
self.engine = Engine()
def start(self):
self.engine.start()Car inheriting from Engine would mean every car is an engine, which is not true and invites calling engine-only methods on a car by mistake. Car holding an Engine means a car has one — the relationship inheritance actually models is "is a more specific version of," and reaching for it to mean "is built from" is the most common overuse of the tool in real codebases.
dataclasses generate the boilerplate
from dataclasses import dataclass
@dataclass
class Point:
x: int
y: intThis one decorator generates __init__, __repr__ and __eq__ from the type-annotated fields alone — the exact three methods written out by hand above. Reaching for @dataclass on a class whose whole job is holding a fixed set of named fields is the modern default; writing all three dunder methods manually is now mostly reserved for classes that need behaviour a dataclass cannot express.
Now practice it
1 challenge · 2 quizzesReading this page is the cheap half. These are the exercises that make you use it.
- OOP — Shape InheritanceProA shape hierarchy — inheritance and method overriding in a single, concrete example.ChallengeSolve · MediumChallengeSolve · Medium
- Object-Oriented Programming · Classes & InstancesClasses and instances, including the dunder methods this page opens on.QuizTake · EasyQuizTake · Easy
- Object-Oriented Programming · Dataclasses & ABCsDataclasses and abstract base classes — the modern shorthand for the boilerplate this page writes by hand.QuizTake · MediumQuizTake · Medium
Read next
Exceptions and Error Handling in Python
What each clause of try/except/else/finally actually guarantees, how raise from chains a new exception to its cause, and why a bare except is almost always wrong.
3 min
Mutable Default Arguments
Why a default value written as a list or dict is created once, shared across every call, and the None-then-create idiom that every codebase uses to avoid it.
3 min