Python · object-oriented

Magic Methods

Double-underscore ("dunder") methods let your own classes work with len(), +, ==, [], print and more.

Syntax is a method call

str(x)→x.__str__() len(x)→x.__len__() x[i]→x.__getitem__(i) a + b→a.__add__(b) a == b→a.__eq__(b) item in s→s.__contains__(item) f(5)→f.__call__(5)

Define the method on the right, and the syntax on the left starts working for your class.

__str__ and __repr__


          # 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)
        
str(p)
p is Point(3, 4)
p.__str__()
Python calls your method
f"Point({3}, {4})" → "Point(3, 4)"
print does the same, then shows the text without quotes

__len__


          # 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
        
len(ml)
ml wraps [1, 2, 3]
ml.__len__()
len() asks the object
len(self.items) → 3
which asks its list

__getitem__ and __setitem__


          # 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
        
ml[0] ml[0] = 9
reading and writing with brackets
ml.__getitem__(0) ml.__setitem__(0, 9)
become these two calls

__add__ and __sub__


          # 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
        
Vector(5) + Vector(3)
the left operand decides
Vector(5).__add__(Vector(3))
self is the left, other the right
Vector(5 + 3) → Vector(8)
__sub__ works the same way for -

__eq__ and __lt__


          # 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
        
Point(5) == Point(5)
two different objects...
Point(5).__eq__(Point(5))
...compared by your rule
5 == 5 → True
without __eq__, == would compare identity and give False

Exercises

Predict each result, then press → to check.

Exercise 1


        # __str__ vs __repr__?
        class MyClass:
            def __str__(self):
                return "str"
            def __repr__(self):
                return "repr"
        str(MyClass())
        # ans: "str"
        repr(MyClass())
        # ans: "repr"
      

Exercise 2


        # __len__ return type?
        class MyClass:
            def __len__(self):
                return 5.5
        len(MyClass())
        # ans: TypeError (__len__ must return an int)
      

Exercise 3


        # __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.

Exercise 4


        # __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__.

Exercise 5


        # __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
      

Exercise 6


          # __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
        
double(5)
an object used like a function
double.__call__(5)
becomes a method call
5 * 2 → 10
self.n is 2

Exercise 7


        # __bool__?
        class MyClass:
            def __bool__(self):
                return False
        bool(MyClass())
        # ans: False
      

So if MyClass(): would skip its block.

Exercise 8


        # __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]
      

Exercise 9


        # __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
      

Exercise 10


        # __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.

Remember

Hooks, not calls

You rarely call dunders yourself; syntax and built-ins call them for you.

Left operand first

a + b tries a.__add__(b).

Return the right type

__len__ must return an int, __bool__ a bool, __str__ a str.

__str__ vs __repr__

Friendly text for users vs an unambiguous one for developers.