Master

Advanced Class Design

Master dunder methods, decorators, and descriptors for powerful classes.

What you will see

Below absolute zero

About 2 min of reading

Quick check

Before this new lesson, one small question about the lesson you just finished. Recall it, do not look it up.

Try It

Give your classes behaviour with dunder methods, class/static methods, and property setters. Each step defines a small class and prints its results.

1

Make a Vector that supports + with __add__

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})" a = Vector(1, 2) b = Vector(3, 4) print(a + b)

Before you run it, say out loud what it will print. Then open the answer.

What it prints
Vector(4, 6)
2

Use a classmethod as an alternative constructor

class Utils: def __init__(self, v): self.v = v @classmethod def from_str(cls, s): return cls(int(s)) u = Utils.from_str("5") print(u.v)

Before you run it, say out loud what it will print. Then open the answer.

What it prints
5
3

Read a staticmethod helper

class Utils: @staticmethod def is_even(n): return n % 2 == 0 print(Utils.is_even(4)) print(Utils.is_even(7))

Before you run it, say out loud what it will print. Then open the answer.

What it prints
True False
4

Validate a value with a property setter

class Temperature: def __init__(self, celsius): self._celsius = celsius @property def celsius(self): return self._celsius @celsius.setter def celsius(self, value): if value < -273.15: raise ValueError("Below absolute zero") self._celsius = value t = Temperature(100) t.celsius = 37 print(t.celsius)

Before you run it, say out loud what it will print. Then open the answer.

What it prints
37
5

Raise an error when a setter rejects the value

class Temperature: def __init__(self, celsius): self._celsius = celsius @property def celsius(self): return self._celsius @celsius.setter def celsius(self, value): if value < -273.15: raise ValueError("Below absolute zero") self._celsius = value t = Temperature(0) try: t.celsius = -300 except ValueError as e: print(e)

Before you run it, say out loud what it will print. Then open the answer.

What it prints
Below absolute zero

Put it in order

Before you write anything, put these lines back in the right order. Click a line to move it up one step. This support step is easier than writing code from scratch, and it is here to make the last example stick.

Data Flow

Dunder methods hook your objects into Python's syntax; the interpreter calls them behind operators and built-ins.
1 Operator or built-in invoked on an object
↓
2 Interpreter looks up the matching dunder method
↓
3 Method runs -> returns a new value or mutates
↓
4 Result used by the caller as if it were native
Line by line

Read the first example one line at a time. A line that prints shows what appears on screen; the rest set things up quietly.

class Vector: sets nothing on screen
def __init__(self, x, y): sets nothing on screen
self.x = x sets nothing on screen
self.y = y sets nothing on screen
def __add__(self, other): sets nothing on screen
return Vector(self.x + other.x, self.y + other.y) sets nothing on screen
def __repr__(self): sets nothing on screen
return f"Vector({self.x}, {self.y})" sets nothing on screen
a = Vector(1, 2) sets nothing on screen
b = Vector(3, 4) sets nothing on screen
print(a + b) Vector(4, 6)
Why this works
The dunder protocol

Double-underscore ("dunder") methods let your classes integrate with Python's language features: __init__, __repr__, __str__, __eq__, __lt__, __iter__, __getitem__, __enter__/__exit__, and more. Implementing a few lets your object work with for, comparisons, with, and arithmetic for free.

When you have a choice

Class vs static vs instance methods

Instance methods receive self and touch instance state. Class methods receive cls and can construct new instances or access class-level data, useful as alternative constructors. Static methods are namespaced helpers with no implicit receiver; use them for logic that does not need class or instance state.

Examples

Dunder methods for operators

Make objects behave like built-ins

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})"

a = Vector(1, 2)
b = Vector(3, 4)
print(a + b)   # Vector(4, 6)

__add__ defines +; __repr__ defines the printable form.

Class and static methods

Different receiver types

class Utils:
    def __init__(self, v):
        self.v = v

    @classmethod
    def from_str(cls, s):
        return cls(int(s))

    @staticmethod
    def is_even(n):
        return n % 2 == 0

u = Utils.from_str("5")
print(Utils.is_even(4))   # True

@classmethod receives the class (cls); @staticmethod receives neither and is a helper function.

Property setters

Mutate computed attributes

class Temperature:
    def __init__(self, celsius):
        self._celsius = celsius

    @property
    def celsius(self):
        return self._celsius

    @celsius.setter
    def celsius(self, value):
        if value < -273.15:
            raise ValueError("Below absolute zero")
        self._celsius = value

A setter lets you validate while keeping attribute syntax.

Words this lesson uses

Each one is defined here on this site. Open the entry for the full definition and the other words it points at.

← Generators and Iterators
Previous Lesson
Generators and Iterators
Next Lesson
Metaprogramming and Decorators
Metaprogramming and Decorators →