Special (Dunder) Methods

Advanced · 18 min read · ▶ live playground · ✦ checkpoint

What if score_a + score_b, len(team), "Mina" in team, and cheer("Ravi") all worked on objects you designed? Dunder methods are double-underscore methods like __add__ and __len__ that Python calls behind familiar syntax, so your classes can feel like normal Python values.

Teach operators your meaning

Operator overloading means defining how operators like +, ==, and < work for objects of your class. You are not changing Python's + for every value. You are teaching Python what + means when the left object is one of yours.

Think of dunder methods as labeled buttons on your class blueprint. When Python sees a + b, it presses the __add__ button on a. When it sees a < b, it presses __lt__.

class Score:
    def __init__(self, player, points):
        self.player = player
        self.points = points
 
    def __add__(self, other):
        combined_player = f"{self.player} + {other.player}"
        combined_points = self.points + other.points
        return Score(combined_player, combined_points)
 
    def __eq__(self, other):
        return self.points == other.points
 
    def __lt__(self, other):
        return self.points < other.points
 
    def __repr__(self):
        return f"Score(player='{self.player}', points={self.points})"
 
 
mina = Score("Mina", 120)
ravi = Score("Ravi", 80)
noor = Score("Noor", 120)
 
print(mina + ravi)
print(mina == noor)
print(ravi < mina)
print(sorted([mina, ravi, noor]))

Score(player='Mina + Ravi', points=200)
True
True
[Score(player='Ravi', points=80), Score(player='Mina', points=120), Score(player='Noor', points=120)]

mina + ravi calls mina.__add__(ravi). mina == noor calls mina.__eq__(noor). ravi < mina calls ravi.__lt__(mina).

The sorted call uses < comparisons, so __lt__ gives Score objects a natural order. Here, "less than" means "has fewer points." That is a design choice. For a different class, < might mean earlier date, lower price, or smaller size.

Follow container protocols

A protocol is a set of method names Python looks for so an object can work with syntax or built-ins. A container protocol is the group of dunder methods that make an object act like a container: __len__ for len(...), __getitem__ for indexing, and __contains__ for in.

class Team:
    def __init__(self, name, players):
        self.name = name
        self.players = players
 
    def __len__(self):
        return len(self.players)
 
    def __getitem__(self, index):
        return self.players[index]
 
    def __contains__(self, player):
        return player in self.players
 
    def summary(self):
        return f"{self.name}: {len(self)} players"
 
 
team = Team("Blue", ["Mina", "Ravi", "Noor"])
 
print(team.summary())
print(team[0])
print(team[-1])
print("Mina" in team)
print("Ada" in team)

Blue: 3 players
Mina
Noor
True
False

len(team) calls team.__len__(). team[0] calls team.__getitem__(0). "Mina" in team calls team.__contains__("Mina").

The object still protects its own meaning. Outside code does not need to know that Team stores a plain list inside self.players. It gets the small public behavior it expects from a container.

Try it - make a roster feel native

Run this, then add one more player to the starting list. Try print(roster[1]) and print("Ada" in roster).

python — playgroundlive
⌘/Ctrl + Enter to run

Make an object callable

A callable object is an object you can call with parentheses, like a function. Define __call__, and Python runs it when you write object(...).

This is useful when an object needs function-like behavior plus memory. A regular function can return a cheer. A callable object can return a cheer and remember how many cheers it has made.

class Cheer:
    def __init__(self, word):
        self.word = word
        self.times_used = 0
 
    def __call__(self, player):
        self.times_used = self.times_used + 1
        return f"{self.word}, {player}! cheer #{self.times_used}"
 
 
cheer = Cheer("Go")
 
print(cheer("Mina"))
print(cheer("Ravi"))
print(cheer.times_used)

Go, Mina! cheer #1
Go, Ravi! cheer #2
2

cheer("Mina") looks like a function call, but cheer is an instance. Python calls cheer.__call__("Mina").

Do not make every object callable. Use __call__ when the object has one clear main action and it helps to keep state beside that action.

Shape a with block

The context-manager protocol is the pair of methods Python uses for with: __enter__ runs at the start of the block, and __exit__ runs when the block ends. You used with open(...) for files earlier; here you see the method shape. The fuller construction story comes in lesson 12.3.

class PracticeLog:
    def __init__(self, player):
        self.player = player
        self.events = []
 
    def __enter__(self):
        self.events.append(f"{self.player} started practice")
        return self
 
    def add(self, event):
        self.events.append(event)
 
    def __exit__(self, exc_type, exc_value, traceback):
        self.events.append(f"{self.player} finished practice")
        for event in self.events:
            print(event)
        return False
 
 
with PracticeLog("Noor") as log:
    log.add("solved one puzzle")
    log.add("earned 30 points")

Noor started practice
solved one puzzle
earned 30 points
Noor finished practice

__enter__ returns the object that becomes log after as. __exit__ receives three exception-related values when something goes wrong inside the block. This example ignores them and returns False, which means "do not hide exceptions."

The big idea is the same as the other protocols: Python syntax asks for a specific dunder method, and your object answers.

Checkpoint

Answer all three to mark this lesson complete

Dunder methods are how your objects join Python's everyday syntax without pretending to be built-in types. Next, you clean up common class patterns with modern tools that write less boilerplate and make intent clearer.

+50 XP on completion