Python — OOP Part 6: Magic Methods

4 min read

Today we’ll look at magic methods — things we use all the time but don’t always have a clear understanding of, and sometimes don’t even realize we’re using. Here’s what a magic method is.

What is a magic method?

  • It’s a special method you can define inside a class that makes the class behave like Python’s built-in types such as int, str, and list.
  • For operators like +, -, >, <, etc., it overloads each one with a method appropriate for the data type and performs the operation in the background.
  • Like __init__ or __str__, the method name is wrapped in double underscores ("__") on both sides.

__init__ and __str__, which you always use when creating a class, are the most representative magic methods and the ones we know best. Many people use them all the time but aren’t sure how to pronounce them. Some say “underscore init underscore,” others say “double underscore init double underscore.” The most idiomatic term is “dunder init dunder.”

We don’t call __init__ directly when we instantiate a class, yet we know it runs under the hood. Let’s see an example.

oop-6.py
class Dog(object):
def __init__(self, name, age):
print('이름: {}, 나이: {}'.format(name, age))

dog_1 = Dog('Pink', '12')
Console output
이름: Pink, 나이: 12

As you can see in the code above, when the class is instantiated, the __init__ method runs automatically. Now let’s look at another kind of magic method. The + and - operators we use without a second thought are also operators that invoke magic methods. When you run x + y, the __add__ magic method on x is called. In the background, x.__add__(y) is what actually runs. Let’s verify this with an example.

First, let’s create a custom class based on the int type.

oop-6.py
# Create a new class with int as its parent
class MyInt(int):
pass

# Create an instance
my_num = MyInt(5)

# Check the type
print(type(my_num))  # => <class '__main__.MyInt'>

# Check whether it's an instance of int
print(isinstance(my_num, int))  # => True

# Check MyInt's base class
print(MyInt.__bases__)  # => (<type 'int'>,)
Console output
<class '__main__.MyInt'>
True
(<class 'int'>,)

We’ve confirmed that MyInt is of type int. Now let’s add it together with a regular int.

oop-6.py
# Create a new class with int as its parent
class MyInt(int):
pass

# Create an instance
my_num = MyInt(5)

# Run addition
print(my_num + 5)  # => 10
Console output
10

It produced the same result as 5 + 5. Let’s check whether my_num really has magic methods.

oop-6.py
# Create a new class with int as its parent
class MyInt(int):
pass

# Create an instance
my_num = MyInt(5)

# Check whether it has magic methods
print(dir(my_num))
Console output
['__abs__', '__add__', '__and__', '__bool__', '__ceil__', '__class__', '__delattr__', '__dict__', '__dir__', '__divmod__', '__doc__', '__eq__', '__float__', '__floor__', '__floordiv__', '__format__', '__ge__', '__getattribute__', '__getnewargs__', '__gt__', '__hash__', '__index__', '__init__', '__int__', '__invert__', '__le__', '__lshift__', '__lt__', '__mod__', '__module__', '__mul__', '__ne__', '__neg__', '__new__', '__or__', '__pos__', '__pow__', '__radd__', '__rand__', '__rdivmod__', '__reduce__', '__reduce_ex__', '__repr__', '__rfloordiv__', '__rlshift__', '__rmod__', '__rmul__', '__ror__', '__round__', '__rpow__', '__rrshift__', '__rshift__', '__rsub__', '__rtruediv__', '__rxor__', '__setattr__', '__sizeof__', '__str__', '__sub__', '__subclasshook__', '__truediv__', '__trunc__', '__xor__', 'bit_length', 'conjugate', 'denominator', 'from_bytes', 'imag', 'numerator', 'real', 'to_bytes']

It inherited a huge number of magic methods from its parent class int. Now let’s call a magic method directly.

oop-6.py
# Create a new class with int as its parent
class MyInt(int):
pass

# Create an instance
my_num = MyInt(5)

# Call the magic method directly
print(my_num.__add__(5))  # => 10
Console output
10

The result is the same as my_num + 5. Now let’s modify the magic method so that the return value is a string instead of an integer.

oop-6.py
# Create a new class with int as its parent
class MyInt(int):
# Override __add__
def __add__(self, other):
return '{} 더하기 {}{} 입니다'.format(self.real, other.real, self.real + other.real)

# Create an instance
my_num = MyInt(5)

print(my_num + 5)  # => 5 더하기 5 는 10 입니다
Console output
5 더하기 510 입니다

How about that? It really did return the result of the addition as a string instead of an integer.

Built-in types like int, str, list, and dict come with magic methods that overload various operators in a way appropriate to their type, all for the user’s convenience. Let’s look at a few more examples.

oop-6.py
# List addition
print([1,2,3] + [4,5,6])
# Addition via the magic method
print([1,2,3].__add__([4,5,6]))

# Check the length of a dictionary
print(len({'one':1, 'two': 2, 'three': 3}))
# Check the length of a dictionary via the magic method
print({'one':1, 'two': 2, 'three': 3}.__len__())
Console output
[1, 2, 3, 4, 5, 6]
[1, 2, 3, 4, 5, 6]
3
3

Do you have a sense of what magic methods are now? But why do we need to know about them? After all, the result is the same whether we know what’s running in the background or not. The reason is so we can apply magic methods to the classes we build ourselves. Let’s try it.

First, let’s create an instance of a simple class.

oop-6.py
class Food(object):
def __init__(self, name, price):
self.name = name
self.price = price

food_1 = Food('아이스크림', 3000)

# Print the instance
print(food_1)
Console output
<__main__.Food object at 0x101a07710>

Printing the instance gave us <__main__.Food object at 0x103cc0ad0>. That’s just the instance’s memory address — not useful to anyone. In a case like this, we define the __str__ magic method to return something more meaningful. Let’s look at an example.

oop-6.py
class Food(object):
def __init__(self, name, price):
self.name = name
self.price = price

    def __str__(self):
        return '아이템: {}, 가격: {}'.format(self.name, self.price)

food_1 = Food('아이스크림', 3000)

# Print the instance
print(food_1)
Console output
아이템: 아이스크림, 가격: 3000

Now let’s create two instances and try a few operators on them.

oop-6.py
class Food(object):
def __init__(self, name, price):
self.name = name
self.price = price

food_1 = Food('아이스크림', 3000)
food_2 = Food('햄버거', 5000)

# Check whether food_2 is greater than food_1
print(food_1 < food_2)
Console output
# In Python 2
False
# In Python 3
Traceback (most recent call last):
File "oop-6.py", line 12, in
print(food_1 < food_2)
TypeError: unorderable types: Food() < Food()

In Python 2 it prints False, but in Python 3 it raises a TypeError. Why? Because Python doesn’t know how to perform the < operation on our Food class. So how did Python 2 produce False? It simply compared the memory addresses of the two instances. Let’s verify that. The example below only works in Python 2.

oop-6.py
class Food(object):
def __init__(self, name, price):
self.name = name
self.price = price

food_1 = Food('아이스크림', 3000)
food_2 = Food('햄버거', 5000)

# Check whether food_2 is greater than food_1
print(food_1)
print(food_2)
print(food_1 < food_2)
Console output
<__main__.Food object at 0x103cc0b50>
<__main__.Food object at 0x1039b5a90>
False

We’ve confirmed that the result simply compared the memory addresses 103cc0b50 and 1039b5a90. Now let’s modify the __lt__ method so that prices get compared instead.

oop-6.py
class Food(object):
def __init__(self, name, price):
self.name = name
self.price = price

    def __lt__(self, other):
        if self.price < other.price:
            return True
        else:
            return False

food_1 = Food('아이스크림', 3000)
food_2 = Food('햄버거', 5000)
food_3 = Food('콜라', 2000)

# Check whether food_2 is greater than food_1
print(food_1 < food_2)  # 3000 < 5000
print(food_2 < food_3)  # 5000 < 2000
Console output
True
False

The price comparison works correctly. Finally, let’s try the __add__ method.

oop-6.py
class Food(object):
def __init__(self, name, price):
self.name = name
self.price = price

food_1 = Food('아이스크림', 3000)
food_2 = Food('햄버거', 5000)

print(food_1 + food_2)
Console output
Traceback (most recent call last):
File "oop-6.py", line 11, in
print(food_1 + food_2)
TypeError: unsupported operand type(s) for +: 'Food' and 'Food'

A TypeError was raised — it says the + operation isn’t supported for these types. Let’s add an __add__ method so that the prices get added together.

oop-6.py
class Food(object):
def __init__(self, name, price):
self.name = name
self.price = price

    def __add__(self, other):
        return self.price + other.price

food_1 = Food('아이스크림', 3000)
food_2 = Food('햄버거', 5000)

print(food_1 + food_2)
Console output
8000

8000, the sum of the ice cream’s price (3000) and the hamburger’s price (5000), printed correctly.

Now that you know what magic methods are, take a look at the various examples below and think about which magic methods you can use in your own classes to make them more convenient. Nice work. Happy coding!

Various magic methods #

OperatorMethod
+object.add(self, other)
object.sub(self, other)
*object.mul(self, other)
//object.floordiv(self, other)
/object.div(self, other)
%object.mod(self, other)
**object.pow(self, other[, modulo])
>>object.lshift(self, other)
<<object.rshift(self, other)
&object.and(self, other)
^object.xor(self, other)
|object.or(self, other)
OperatorMethod
+=object.iadd(self, other)
-=object.isub(self, other)
*=object.imul(self, other)
/=object.idiv(self, other)
//=object.ifloordiv(self, other)
%=object.imod(self, other)
**=object.ipow(self, other[, modulo])
<<=object.ilshift(self, other)
=object.irshift(self, other)
&=object.iand(self, other)
^=object.ixor(self, other)
|== object.ior(self, other)
OperatorMethod
object.neg(self)
+object.pos(self)
abs()object.abs(self)
~object.invert(self)
complex()object.complex(self)
int()object.int(self)
long()object.long(self)
float()object.float(self)
oct()object.oct(self)
hex()object.hex(self)
OperatorMethod
<object.lt(self, other)
<=object.le(self, other)
==object.eq(self, other)
!=object.ne(self, other)
>=object.ge(self, other)
>object.gt(self, other)
X