Special Methods
Why does print([1, 2]) show the list nicely, len("abc") give 3, and + join two strings? Because those types define special methods, also called dunder methods since their names start and end with double underscores, like __len__. When you define them in your own classes, your objects work with Python's built-in functions and operators too.
The problem
By default, printing an object isn't very helpful:
class Book:
def __init__(self, title, author):
self.title = title
self.author = author
book = Book("Dune", "Frank Herbert")
print(book)__str__: a readable description
__str__ returns the text that print() and str() show. Write it for the people using your program:
class Book:
def __init__(self, title, author):
self.title = title
self.author = author
def __str__(self):
return f"{self.title} by {self.author}"
book = Book("Dune", "Frank Herbert")
print(book)
print(f"I'm reading {book}")__repr__: a description for developers
__repr__ returns an unambiguous description, ideally one that looks like the code to recreate the object. It's what you see when an object appears inside a list, or with repr():
class Book:
def __init__(self, title, author):
self.title = title
self.author = author
def __str__(self):
return f"{self.title} by {self.author}"
def __repr__(self):
return f"Book({self.title!r}, {self.author!r})"
books = [Book("Dune", "Frank Herbert"), Book("Emma", "Jane Austen")]
print(books) # lists use __repr__ for their items
print(books[0]) # print uses __str__If you write only one of the two, write __repr__: Python falls back to it when there's no __str__.
__eq__: comparing with ==
By default, == checks whether two variables are the same object, not whether they hold the same values:
class Point:
def __init__(self, x, y):
self.x = x
self.y = y
print(Point(1, 2) == Point(1, 2))Define __eq__ to decide what "equal" means for your class:
class Point:
def __init__(self, x, y):
self.x = x
self.y = y
def __eq__(self, other):
return self.x == other.x and self.y == other.y
print(Point(1, 2) == Point(1, 2))
print(Point(1, 2) == Point(3, 4))
print(Point(1, 2) != Point(3, 4)) # != works automatically__lt__: ordering and sorting
__lt__ defines < (less than). With it, sorted(), min() and max() work on your objects without a key:
class Player:
def __init__(self, name, score):
self.name = name
self.score = score
def __lt__(self, other):
return self.score < other.score
def __repr__(self):
return f"{self.name}({self.score})"
players = [Player("Mia", 1200), Player("Leo", 1850), Player("Ava", 990)]
print(sorted(players))
print(max(players))
print(players[0] < players[1])The other comparisons have special methods too: __le__ (<=), __gt__ (>) and __ge__ (>=).
__len__, __getitem__ and __contains__
These three let your object act like a collection:
__len__makeslen(obj)work.__getitem__makesobj[index]work, and that's enough to loop over it too.__contains__makesitem in objwork.
class Playlist:
def __init__(self, name, songs):
self.name = name
self.songs = list(songs)
def __len__(self):
return len(self.songs)
def __getitem__(self, index):
return self.songs[index]
def __contains__(self, song):
return song in self.songs
mix = Playlist("Road trip", ["Africa", "Wonderwall", "Hey Jude"])
print(len(mix))
print(mix[0])
print(mix[-1])
print("Hey Jude" in mix)
for song in mix:
print("-", song)Arithmetic operators
Define __add__, __sub__, __mul__ and friends to make +, - and * work. This is called operator overloading:
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 __mul__(self, factor):
return Vector(self.x * factor, self.y * factor)
def __repr__(self):
return f"Vector({self.x}, {self.y})"
a = Vector(1, 2)
b = Vector(3, 4)
print(a + b)
print(a * 3)| Operator | Special method |
|---|---|
a + b |
__add__ |
a - b |
__sub__ |
a * b |
__mul__ |
a / b |
__truediv__ |
a == b |
__eq__ |
a < b |
__lt__ |
len(a) |
__len__ |
a[i] |
__getitem__ |
x in a |
__contains__ |
str(a) / print(a) |
__str__ |
repr(a) |
__repr__ |
A shortcut: dataclasses
Writing __init__, __repr__ and __eq__ by hand gets repetitive. The dataclasses module writes them for you:
from dataclasses import dataclass
@dataclass
class Point:
x: int
y: int
p = Point(3, 4)
print(p)
print(p == Point(3, 4))
print(p.x + p.y)The x: int lines are type hints, covered in the Type Hints lesson. For simple classes that mainly hold data, dataclasses save a lot of typing.
Exercises
Exercise 1: Printable money
Add __str__ to the Money class so that print() shows 12.50 EUR.
class Money:
def __init__(self, amount, currency):
self.amount = amount
self.currency = currency
print(Money(12.5, "EUR"))Add a method called __str__ that returns a string.
Format the amount with two decimals: f"{self.amount:.2f} {self.currency}".
Exercise 2: Adding money
Now add __add__ so two Money objects with the same currency can be added. If the currencies differ, raise a ValueError.
class Money:
def __init__(self, amount, currency):
self.amount = amount
self.currency = currency
def __str__(self):
return f"{self.amount:.2f} {self.currency}"
print(Money(12.5, "EUR") + Money(7.25, "EUR")) # 19.75 EUR__add__(self, other) runs when you write a + b. It should return a new Money object.
Compare self.currency with other.currency first, and raise a ValueError if they differ.
Exercise 3: Equal cards
Add __eq__ so two cards are equal when both rank and suit match, and __repr__ so a card shows as Card('A', 'spades').
class Card:
def __init__(self, rank, suit):
self.rank = rank
self.suit = suit
print(Card("A", "spades") == Card("A", "spades")) # True
print(Card("A", "spades") == Card("K", "spades")) # False
print([Card("Q", "hearts")]) # [Card('Q', 'hearts')]__eq__(self, other) should return True only when both rank and suit match.
In __repr__, !r adds the quotes for you: f"Card({self.rank!r}, {self.suit!r})".
Exercise 4: A word bag
Give WordBag a __len__ that returns the number of words and a __contains__ that checks for a word, ignoring case.
class WordBag:
def __init__(self, text):
self.words = text.lower().split()
bag = WordBag("The quick brown fox")
print(len(bag)) # 4
print("QUICK" in bag) # True
print("dog" in bag) # False__len__ returns the number of words: len(self.words).
__contains__(self, word) should lowercase the word before checking whether it's in self.words.
Special methods at a glance
A quick reference for later. You never call these methods yourself; Python calls them when you use the matching syntax.
| Method | Python calls it for | Typical use |
|---|---|---|
__init__(self, ...) |
Point(1, 2) |
Setting up a new object |
__str__(self) |
print(p), str(p), f"{p}" |
A readable description for users |
__repr__(self) |
repr(p), and when the object is shown inside a list |
An exact description for developers |
__eq__(self, other) |
p == q (and !=) |
Comparing two objects by value |
__lt__(self, other) |
p < q, and sorted(), min(), max() |
Ordering objects |
__le__, __gt__, __ge__ |
<=, >, >= |
The other comparisons |
__len__(self) |
len(p) |
The object's size |
__getitem__(self, key) |
p[key], and for loops if there's no __iter__ |
Indexing like a list or dictionary |
__contains__(self, item) |
item in p |
Membership tests |
__iter__(self) |
for x in p |
Looping (see Iterators and Generators) |
__add__(self, other) |
p + q |
Adding objects |
__sub__, __mul__, __truediv__ |
-, *, / |
The other arithmetic operators |
__bool__(self) |
if p:, bool(p) |
Deciding whether the object counts as true |
__call__(self, ...) |
p(...) |
Letting an object be called like a function |
__hash__(self) |
Putting the object in a set or using it as a dictionary key | Usually written alongside __eq__ |
@dataclass writes __init__, __repr__ and __eq__ for you, as you saw above.
Summary
- Special (dunder) methods let your objects work with built-in functions and operators.
__str__is for readable output, and__repr__is the developer view, used inside lists.__eq__defines==, and__lt__defines<, which makes sorting work.__len__,__getitem__and__contains__make an object behave like a collection.__add__,__mul__and friends overload arithmetic operators.@dataclassgenerates__init__,__repr__and__eq__for simple data classes.