Double-underscore ("dunder") methods let your own classes work with len(), +, ==, [], print and more.
Define the method on the right, and the syntax on the left starts working for your class.
# string representation
class Point:
def __init__(self, x, y):
self.x = x
self.y = y
def __str__(self):
return f"Point({self.x}, {self.y})"
str(Point(3, 4))
# ans: "Point(3, 4)"
print(Point(3, 4))
# ans: Point(3, 4)
print does the same, then shows the text without quotes
# len() support
class MyList:
def __init__(self, items):
self.items = items
def __len__(self):
return len(self.items)
len(MyList([1, 2, 3]))
# ans: 3
# indexing support
class MyList:
def __init__(self):
self.items = []
def __getitem__(self, index):
return self.items[index]
def __setitem__(self, index, value):
self.items[index] = value
ml = MyList()
ml.items = [1, 2, 3]
ml[0]
# ans: 1
# operator overloading
class Vector:
def __init__(self, x):
self.x = x
def __add__(self, other):
return Vector(self.x + other.x)
Vector(5) + Vector(3)
# ans: a new Vector with x = 8
__sub__ works the same way for -
# comparison operators
class Point:
def __init__(self, x):
self.x = x
def __eq__(self, other):
return self.x == other.x
def __lt__(self, other):
return self.x < other.x
Point(5) == Point(5)
# ans: True
__eq__, == would compare identity and give FalsePredict each result, then press → to check.
# __str__ vs __repr__?
class MyClass:
def __str__(self):
return "str"
def __repr__(self):
return "repr"
str(MyClass())
# ans: "str"
repr(MyClass())
# ans: "repr"
# __len__ return type?
class MyClass:
def __len__(self):
return 5.5
len(MyClass())
# ans: TypeError (__len__ must return an int)
# __getitem__ for iteration?
class MyClass:
def __getitem__(self, i):
if i >= 3:
raise IndexError
return i
for x in MyClass():
print(x)
# ans: 0, 1, 2
With no __iter__, a for loop calls __getitem__(0), (1), … until IndexError.
# __add__ with an int?
class MyClass:
def __init__(self, x):
self.x = x
def __add__(self, other):
return MyClass(self.x + other)
MyClass(5) + 3
# ans: a new MyClass with x = 8
3 + MyClass(5) would fail: that needs __radd__.
# __contains__?
class MySet:
def __init__(self, items):
self.items = set(items)
def __contains__(self, item):
return item in self.items
2 in MySet([1, 2, 3])
# ans: True
# __call__?
class Multiplier:
def __init__(self, n):
self.n = n
def __call__(self, x):
return x * self.n
double = Multiplier(2)
double(5)
# ans: 10
self.n is 2
# __bool__?
class MyClass:
def __bool__(self):
return False
bool(MyClass())
# ans: False
So if MyClass(): would skip its block.
# __iter__?
class MyRange:
def __init__(self, n):
self.n = n
def __iter__(self):
return iter(range(self.n))
list(MyRange(3))
# ans: [0, 1, 2]
# __enter__ and __exit__?
class MyContext:
def __enter__(self):
return self
def __exit__(self, *args):
pass
with MyContext() as ctx:
pass
# ans: __enter__ runs as the block starts, __exit__ as it ends
# __del__?
class MyClass:
def __del__(self):
print("Deleted")
obj = MyClass()
del obj
# ans: prints Deleted
del removes the name; CPython calls __del__ when no references remain.
You rarely call dunders yourself; syntax and built-ins call them for you.
a + b tries a.__add__(b).
__len__ must return an int, __bool__ a bool, __str__ a str.
__str__ vs __repr__Friendly text for users vs an unambiguous one for developers.