Contents
  1. Class Structure
  2. __str__ and __repr__
  3. __len__
  4. __add__ and Arithmetic Dunders
  5. __eq__ and Comparison Dunders
  6. __getitem__ and __setitem__
  7. __iter__ and __next__
  8. __call__
  9. __getattr__ and __setattr__
  10. __dict__
  11. __del__
  12. What You Can Do Now
  13. All Dunders at a Glance
← All posts

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)
OperatorDunder
+__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
OperatorDunder
==__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

DunderTriggered 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
← All posts