Python Classes and Dunder Methods: A Quick Reference
A practical reference for Python class structure and the most important dunder methods, with concise examples for each.
Dunder methods (double underscore methods) define how your objects behave with Python’s built-in syntax. They are not called directly. Python calls them behind the scenes when you use operators, built-in functions, or language constructs on your objects.
Class Structure
class Product:
def __init__(self, name, price):
self.name = name
self.price = price
__init__ runs when you instantiate the class. self refers to the instance being created.
p = Product("Notebook", 12.99)
__str__ and __repr__
__str__ is what print() and str() use. Intended for human-readable output.
__repr__ is what the REPL and repr() use. Intended to be unambiguous, ideally reproducible.
class Product:
def __init__(self, name, price):
self.name = name
self.price = price
def __str__(self):
return f"{self.name} (${self.price})"
def __repr__(self):
return f"Product(name={self.name!r}, price={self.price!r})"
p = Product("Notebook", 12.99)
print(p) # Notebook ($12.99)
repr(p) # Product(name='Notebook', price=12.99)
__len__
Called by len().
class Cart:
def __init__(self):
self.items = []
def add(self, item):
self.items.append(item)
def __len__(self):
return len(self.items)
cart = Cart()
cart.add("apple")
cart.add("banana")
len(cart) # 2
__add__ and Arithmetic Dunders
Called by the + operator. Equivalent methods exist for other operators.
class Vector:
def __init__(self, x, y):
self.x = x
self.y = y
def __add__(self, other):
return Vector(self.x + other.x, self.y + other.y)
def __repr__(self):
return f"Vector({self.x}, {self.y})"
v1 = Vector(1, 2)
v2 = Vector(3, 4)
v1 + v2 # Vector(4, 6)
| Operator | Dunder |
|---|---|
+ | __add__ |
- | __sub__ |
* | __mul__ |
/ | __truediv__ |
// | __floordiv__ |
% | __mod__ |
__eq__ and Comparison Dunders
Called by ==. Without it, Python compares object identity, not value.
class Product:
def __init__(self, name, price):
self.name = name
self.price = price
def __eq__(self, other):
return self.name == other.name and self.price == other.price
def __lt__(self, other):
return self.price < other.price
p1 = Product("Notebook", 12.99)
p2 = Product("Notebook", 12.99)
p1 == p2 # True
p1 < Product("Pen", 1.99) # False
| Operator | Dunder |
|---|---|
== | __eq__ |
!= | __ne__ |
< | __lt__ |
<= | __le__ |
> | __gt__ |
>= | __ge__ |
__getitem__ and __setitem__
__getitem__ is called by obj[key]. __setitem__ is called by obj[key] = value.
class Registry:
def __init__(self):
self._data = {}
def __getitem__(self, key):
return self._data[key]
def __setitem__(self, key, value):
self._data[key] = value
r = Registry()
r["user"] = "brian"
r["user"] # "brian"
__iter__ and __next__
__iter__ makes an object usable in a for loop. __next__ defines what each iteration returns. Raise StopIteration when exhausted.
class Countdown:
def __init__(self, start):
self.current = start
def __iter__(self):
return self
def __next__(self):
if self.current <= 0:
raise StopIteration
self.current -= 1
return self.current + 1
for n in Countdown(3):
print(n)
# 3
# 2
# 1
__call__
Makes an instance callable like a function.
class Multiplier:
def __init__(self, factor):
self.factor = factor
def __call__(self, value):
return value * self.factor
double = Multiplier(2)
double(5) # 10
double(9) # 18
__getattr__ and __setattr__
__getattr__ is called only when normal attribute lookup fails, not on every access.
__setattr__ is called on every attribute assignment, including inside __init__. Be careful not to cause infinite recursion.
class FlexObject:
def __init__(self):
self._store = {}
def __getattr__(self, name):
return self._store.get(name, None)
def __setattr__(self, name, value):
if name == "_store":
super().__setattr__(name, value) # avoid recursion
else:
self._store[name] = value
obj = FlexObject()
obj.color = "blue"
obj.color # "blue"
obj.missing # None
__dict__
Not a method. A special attribute. A dictionary of all instance attributes and their current values.
class Product:
def __init__(self, name, price):
self.name = name
self.price = price
p = Product("Notebook", 12.99)
p.__dict__ # {'name': 'Notebook', 'price': 12.99}
Useful for inspection, serialisation, and debugging.
__del__
Called when the object is about to be garbage collected. Rarely needed and difficult to rely on. Python’s garbage collector does not guarantee when or in what order __del__ runs.
class Connection:
def __del__(self):
print("Connection closed")
Prefer context managers (__enter__ and __exit__) for deterministic resource cleanup.
What You Can Do Now
Build a Saiyan class that uses as many of these dunders as possible. The goal is to model a fighter whose power level can be compared, added, printed, iterated over as a transformation sequence, and called to trigger Ultra Instinct.
class Saiyan:
TRANSFORMATIONS = ["Base", "Super Saiyan", "Super Saiyan Blue", "Ultra Instinct"]
def __init__(self, name, power_level):
self.name = name
self.power_level = power_level
self._index = 0
def __str__(self):
return f"{self.name} | Power Level: {self.power_level}"
def __repr__(self):
return f"Saiyan(name={self.name!r}, power_level={self.power_level!r})"
def __add__(self, other):
combined = self.power_level + other.power_level
return Saiyan(f"{self.name} & {other.name}", combined)
def __eq__(self, other):
return self.power_level == other.power_level
def __lt__(self, other):
return self.power_level < other.power_level
def __len__(self):
return len(self.TRANSFORMATIONS)
def __getitem__(self, index):
return self.TRANSFORMATIONS[index]
def __iter__(self):
self._index = 0
return self
def __next__(self):
if self._index >= len(self.TRANSFORMATIONS):
raise StopIteration
form = self.TRANSFORMATIONS[self._index]
self._index += 1
return form
def __call__(self):
return f"{self.name} has awakened Ultra Instinct. Instinct surpasses thought."
def __del__(self):
print(f"{self.name} has fallen.")
goku = Saiyan("Goku", 9001)
vegeta = Saiyan("Vegeta", 8500)
print(goku) # Goku | Power Level: 9001
repr(goku) # Saiyan(name='Goku', power_level=9001)
print(goku > vegeta) # True
print(goku + vegeta) # Goku & Vegeta | Power Level: 17501
print(len(goku)) # 4
print(goku[3]) # Ultra Instinct
for form in goku:
print(form)
# Base
# Super Saiyan
# Super Saiyan Blue
# Ultra Instinct
print(goku()) # Goku has awakened Ultra Instinct. Instinct surpasses thought.
print(goku.__dict__) # {'name': 'Goku', 'power_level': 9001, '_index': 4}
Run this, then extend it. Add __mul__ to multiply power levels when a Saiyan goes Super Saiyan. Add __setattr__ to reject any power level below zero. Add __getattr__ to return "Unknown" for any attribute that does not exist.
All Dunders at a Glance
| Dunder | Triggered by |
|---|---|
__init__ | ClassName(...) |
__str__ | print(obj), str(obj) |
__repr__ | repr(obj), REPL output |
__len__ | len(obj) |
__add__ | obj + other |
__eq__ | obj == other |
__lt__ | obj < other |
__getitem__ | obj[key] |
__setitem__ | obj[key] = value |
__iter__ | for x in obj |
__next__ | next value in iteration |
__call__ | obj(...) |
__getattr__ | obj.missing_attr |
__setattr__ | obj.attr = value |
__dict__ | obj.__dict__ |
__del__ | garbage collection |