Python course Β· Module 3: Object-Oriented Programming
Magic methods - hidden animal behaviors
In this lesson11
Welcome back! Darwin here with a fascinating lesson about Python's hidden powers.
In nature, animals have hidden behaviors - a lion roars, an elephant trumpets, a chameleon camouflages. In Python, classes can also have hidden behaviors through magic methods - special methods with double underscores!
1lion = Animal("Lion")
2print(lion) # How does Python know what to display? __str__!
3len(lion) # How does Python know what to measure? __len__!
4lion + elephant # How does Python know what to add? __add__!What are magic methods?
Magic methods (also called dunder methods from "double underscore") are special methods with names in the format __method__ that Python automatically calls in specific situations.
Safari Analogy: They're like natural instincts of animals - you don't have to teach them, they're built into their nature!
1class Animal:
2 def __init__(self, name):
3 """Called when CREATING an object"""
4 self.name = name
5
6 def __str__(self):
7 """Called by print() and str()"""
8 return f"Animal: {self.name}"
9
10 def __len__(self):
11 """Called by len()"""
12 return len(self.name)
13
14lion = Animal("Lion") # Calls __init__
15print(lion) # Calls __str__
16print(len(lion)) # Calls __len__Why "magic"?
- Python calls them automatically
- They make your classes behave like built-in types
- They allow using standard Python syntax
Basic magic methods
1. __init__ - Constructor
You already know this one! Called when creating an object.
1class Species:
2 def __init__(self, name, population):
3 """Initialization - you already know this!"""
4 self.name = name
5 self.population = population
6
7lion = Species("Lion", 500) # Calls __init__2. __str__ - Human-readable representation
Called by print() and str() - returns a user-friendly string. When you write print(lion), print() first converts the argument to text with str(lion), and str() calls the method lion.__str__().
1class Animal:
2 def __init__(self, name, age):
3 self.name = name
4 self.age = age
5
6 def __str__(self):
7 """For humans - readable"""
8 return f"{self.name} (age: {self.age} years)"
9
10lion = Animal("Simba", 5)
11print(lion) # "Simba (age: 5 years)" - calls __str__
12print(str(lion)) # Same thing3. __repr__ - Programmer representation
Called by repr() and in the console - returns an unambiguous object representation.
1class Animal:
2 def __init__(self, name, age):
3 self.name = name
4 self.age = age
5
6 def __repr__(self):
7 """For programmers - precise"""
8 return f"Animal(name='{self.name}', age={self.age})"
9
10 def __str__(self):
11 """For users - readable"""
12 return f"{self.name} ({self.age} years)"
13
14lion = Animal("Simba", 5)
15
16# In Python console
17>>> lion
18Animal(name='Simba', age=5) # Calls __repr__
19
20# In print
21print(lion) # "Simba (5 years)" - calls __str__
22
23# repr() explicitly
24print(repr(lion)) # "Animal(name='Simba', age=5)"Rule: __repr__ should return code that can recreate the object!
1class Point:
2 def __init__(self, x, y):
3 self.x = x
4 self.y = y
5
6 def __repr__(self):
7 return f"Point({self.x}, {self.y})"
8
9p = Point(3, 4)
10print(repr(p)) # "Point(3, 4)" - you can copy and paste!Comparison methods
Allow comparing objects with ==, <, >, etc.
1class Animal:
2 def __init__(self, name, weight):
3 self.name = name
4 self.weight = weight # kg
5
6 def __eq__(self, other):
7 """Equality: =="""
8 if not isinstance(other, Animal):
9 return False
10 return self.weight == other.weight
11
12 def __lt__(self, other):
13 """Less than: <"""
14 if not isinstance(other, Animal):
15 return NotImplemented
16 return self.weight < other.weight
17
18 def __le__(self, other):
19 """Less than or equal: <="""
20 return self.weight <= other.weight
21
22 def __gt__(self, other):
23 """Greater than: >"""
24 return self.weight > other.weight
25
26 def __ge__(self, other):
27 """Greater than or equal: >="""
28 return self.weight >= other.weight
29
30 def __ne__(self, other):
31 """Not equal: !="""
32 return not self.__eq__(other)
33
34lion = Animal("Lion", 190)
35elephant = Animal("Elephant", 5000)
36rhino = Animal("Rhino", 2300)
37
38print(lion < elephant) # True - calls __lt__
39print(elephant > rhino) # True - calls __gt__
40print(lion == Animal("Tiger", 190)) # True - same weightTip: You can use the @functools.total_ordering decorator - define only __eq__ and one of __lt__, __le__, __gt__, __ge__, and the rest will be automatically generated!
1from functools import total_ordering
2
3@total_ordering
4class Animal:
5 def __init__(self, name, weight):
6 self.name = name
7 self.weight = weight
8
9 def __eq__(self, other):
10 """Only this and __lt__ - the rest automatically!"""
11 if not isinstance(other, Animal):
12 return False
13 return self.weight == other.weight
14
15 def __lt__(self, other):
16 """Only this and __eq__ - the rest automatically!"""
17 if not isinstance(other, Animal):
18 return NotImplemented
19 return self.weight < other.weight
20
21 # __le__, __gt__, __ge__, __ne__ - automatically generated!Arithmetic methods
Allow using mathematical operators with objects!
1class Population:
2 """Represents a species population"""
3
4 def __init__(self, species, count):
5 self.species = species
6 self.count = count
7
8 def __add__(self, other):
9 """Addition: +"""
10 if isinstance(other, Population):
11 if self.species != other.species:
12 raise ValueError("Different species!")
13 return Population(self.species, self.count + other.count)
14 elif isinstance(other, int):
15 return Population(self.species, self.count + other)
16 return NotImplemented
17
18 def __sub__(self, other):
19 """Subtraction: -"""
20 if isinstance(other, int):
21 return Population(self.species, max(0, self.count - other))
22 return NotImplemented
23
24 def __mul__(self, other):
25 """Multiplication: *"""
26 if isinstance(other, (int, float)):
27 return Population(self.species, int(self.count * other))
28 return NotImplemented
29
30 def __str__(self):
31 return f"{self.species}: {self.count} individuals"
32
33# Using operators!
34lions_north = Population("Lion", 120)
35lions_south = Population("Lion", 85)
36
37# Adding populations
38total_lions = lions_north + lions_south
39print(total_lions) # "Lion: 205 individuals"
40
41# Adding a number
42more_lions = lions_north + 30
43print(more_lions) # "Lion: 150 individuals"
44
45# Subtraction (population decrease)
46fewer_lions = lions_north - 20
47print(fewer_lions) # "Lion: 100 individuals"
48
49# Multiplication (estimating growth)
50projected = lions_north * 1.5 # 50% growth
51print(projected) # "Lion: 180 individuals"Container methods
Make your objects behave like lists, dictionaries!
__len__ - Length
When a class has a __len__ method, the len() function works on its objects just like on a list. The pack returns the number of its members:
1class Pack:
2 """Animal pack"""
3
4 def __init__(self, species):
5 self.species = species
6 self.members = []
7
8 def add(self, name):
9 self.members.append(name)
10
11 def __len__(self):
12 """Called by len()"""
13 return len(self.members)
14
15pack = Pack("Lions")
16pack.add("Simba")
17pack.add("Nala")
18pack.add("Mufasa")
19
20print(len(pack)) # 3 - calls __len____getitem__ and __setitem__ - Index access
These two methods let you read and change an object's elements with square brackets, as in a list. Along the way the habitat also gets __contains__, which handles the in operator:
1class Habitat:
2 """Habitat with animals"""
3
4 def __init__(self, name):
5 self.name = name
6 self.animals = []
7
8 def __getitem__(self, index):
9 """Read access: habitat[0]"""
10 return self.animals[index]
11
12 def __setitem__(self, index, value):
13 """Write access: habitat[0] = "Lion" """
14 self.animals[index] = value
15
16 def __len__(self):
17 return len(self.animals)
18
19 def __contains__(self, item):
20 """Operator 'in': "Lion" in habitat"""
21 return item in self.animals
22
23 def append(self, animal):
24 self.animals.append(animal)
25
26savanna = Habitat("Savanna")
27savanna.append("Lion")
28savanna.append("Elephant")
29savanna.append("Giraffe")
30
31# Access like a list!
32print(savanna[0]) # "Lion" - calls __getitem__
33savanna[1] = "Rhino" # Calls __setitem__
34
35# Operator in
36print("Lion" in savanna) # True - calls __contains__
37
38# len()
39print(len(savanna)) # 3 - calls __len__
40
41# Iteration (automatic through __getitem__)
42for animal in savanna:
43 print(animal)__iter__ and __next__ - Iteration
A for loop first calls __iter__ and then __next__ on every step until it gets a StopIteration exception. That is how we will build a calendar of expedition days:
1class Expedition:
2 """Iterator over expedition days"""
3
4 def __init__(self, days):
5 self.days = days
6 self.current_day = 0
7
8 def __iter__(self):
9 """Return iterator (self)"""
10 self.current_day = 0
11 return self
12
13 def __next__(self):
14 """Return next element"""
15 if self.current_day >= self.days:
16 raise StopIteration # End of iteration
17
18 self.current_day += 1
19 return f"Day {self.current_day}"
20
21expedition = Expedition(5)
22
23# Iteration with for
24for day in expedition:
25 print(day)
26# Day 1
27# Day 2
28# Day 3
29# Day 4
30# Day 5
31
32# Or manually
33exp2 = Expedition(3)
34print(next(exp2)) # "Day 1"
35print(next(exp2)) # "Day 2"
36print(next(exp2)) # "Day 3"A fourth call to next(exp2) would raise StopIteration. It is the same signal that makes a for loop stop on its own.
__call__ - Callable objects
Makes an object callable like a function!
1class AnimalSound:
2 """Callable - object like a function"""
3
4 def __init__(self, species, sound):
5 self.species = species
6 self.sound = sound
7
8 def __call__(self, times=1):
9 """Call object like a function"""
10 return " ".join([self.sound] * times)
11
12lion_roar = AnimalSound("Lion", "ROAR")
13elephant_trumpet = AnimalSound("Elephant", "TRUUU")
14
15# Call like a function!
16print(lion_roar()) # "ROAR" - calls __call__
17print(lion_roar(3)) # "ROAR ROAR ROAR"
18print(elephant_trumpet(2)) # "TRUUU TRUUU"Context managers - __enter__ and __exit__
Allow using the with statement!
1class SafariCamera:
2 """Context manager for taking photos"""
3
4 def __init__(self, location):
5 self.location = location
6 self.photos = []
7
8 def __enter__(self):
9 """Called when entering the 'with' block"""
10 print(f"Turning on camera in {self.location}")
11 return self # Return object for use
12
13 def __exit__(self, exc_type, exc_val, exc_tb):
14 """Called when exiting the 'with' block"""
15 print(f"Photos saved: {len(self.photos)}")
16 print(f"Turning off camera")
17 return False # Don't suppress exceptions
18
19 def take_photo(self, subject):
20 """Take a photo"""
21 self.photos.append(subject)
22 print(f" Photo: {subject}")
23
24# Usage with 'with' - automatic __enter__ and __exit__!
25with SafariCamera("Serengeti") as camera:
26 camera.take_photo("Lion hunting")
27 camera.take_photo("Elephant herd")
28 camera.take_photo("Giraffe by a tree")When you run it, the console shows:
1Turning on camera in Serengeti
2 Photo: Lion hunting
3 Photo: Elephant herd
4 Photo: Giraffe by a tree
5Photos saved: 3
6Turning off cameraWe never called __enter__ or __exit__ by hand. with ran the first method at the start of the block and the second one automatically at its end, and it would do so even if an error happened inside.
Safari example - complete cataloging system
Finally, a species class that uses almost every magic method from this lesson: its objects have a readable description, compare by population, support + and -, behave like a container and can be called like a function.
1from functools import total_ordering
2from datetime import datetime
3
4@total_ordering
5class Species:
6 """
7 Species class with a full set of magic methods
8
9 Behaves like a built-in Python type!
10 """
11
12 all_species = [] # Registry of all species
13
14 def __init__(self, scientific_name, common_name, population, habitat):
15 """Constructor"""
16 self.scientific_name = scientific_name
17 self.common_name = common_name
18 self.population = population
19 self.habitat = habitat
20 self.observations = []
21
22 Species.all_species.append(self)
23
24 # === REPRESENTATION ===
25
26 def __str__(self):
27 """For print() - human-friendly"""
28 return f"{self.common_name} ({self.population} individuals)"
29
30 def __repr__(self):
31 """For repr() - unambiguous"""
32 return (f"Species(scientific_name='{self.scientific_name}', "
33 f"common_name='{self.common_name}', "
34 f"population={self.population}, "
35 f"habitat='{self.habitat}')")
36
37 # === COMPARISONS (only __eq__ and __lt__, rest via @total_ordering) ===
38
39 def __eq__(self, other):
40 """Equality: =="""
41 if not isinstance(other, Species):
42 return False
43 return self.population == other.population
44
45 def __lt__(self, other):
46 """Less than: < (comparison by population)"""
47 if not isinstance(other, Species):
48 return NotImplemented
49 return self.population < other.population
50
51 def __hash__(self):
52 """Hash from the same field as __eq__ - equal objects must have equal hashes"""
53 return hash(self.population)
54
55 # === ARITHMETIC ===
56
57 def __add__(self, other):
58 """Addition: species + 50"""
59 if isinstance(other, int):
60 return Species(
61 self.scientific_name,
62 self.common_name,
63 self.population + other,
64 self.habitat
65 )
66 return NotImplemented
67
68 def __sub__(self, other):
69 """Subtraction: species - 20"""
70 if isinstance(other, int):
71 return Species(
72 self.scientific_name,
73 self.common_name,
74 max(0, self.population - other),
75 self.habitat
76 )
77 return NotImplemented
78
79 # === CONTAINER ===
80
81 def __len__(self):
82 """len(species) - number of observations"""
83 return len(self.observations)
84
85 def __getitem__(self, index):
86 """species[0] - access to observations"""
87 return self.observations[index]
88
89 def __contains__(self, location):
90 """'Serengeti' in species - check location"""
91 return any(obs["location"] == location for obs in self.observations)
92
93 # === CALLABLE ===
94
95 def __call__(self, location, count):
96 """Call like a function - add observation"""
97 observation = {
98 "date": datetime.now().strftime("%Y-%m-%d"),
99 "location": location,
100 "count": count
101 }
102 self.observations.append(observation)
103 return f"Added: {count}x {self.common_name} in {location}"
104
105 # === BOOL ===
106
107 def __bool__(self):
108 """bool(species) - does the species have any population?"""
109 return self.population > 0
110
111# === DEMONSTRATION OF ALL MAGIC METHODS ===
112
113print("=== CREATING SPECIES ===\n")
114
115lion = Species("Panthera leo", "Lion", 120, "savanna")
116elephant = Species("Loxodonta africana", "Elephant", 450, "savanna")
117rhino = Species("Diceros bicornis", "Rhino", 45, "savanna")
118extinct = Species("Dodo", "Dodo", 0, "Mauritius")
119
120# __str__ and __repr__
121print("__str__ (print):")
122print(lion) # "Lion (120 individuals)"
123
124print("\n__repr__ (repr):")
125print(repr(lion))
126# Species(scientific_name='Panthera leo', common_name='Lion', population=120, habitat='savanna')
127
128# Comparisons (__eq__, __lt__, etc.)
129print("\n=== COMPARISONS ===")
130print(f"lion == elephant? {lion == elephant}") # False
131print(f"rhino < lion? {rhino < lion}") # True (45 < 120)
132print(f"elephant > lion? {elephant > lion}") # True (450 > 120)
133
134# Sorting (works through __lt__)
135species_list = [lion, rhino, elephant]
136species_list.sort()
137print(f"\nSorted (ascending): {[str(s) for s in species_list]}")
138# ['Rhino (45 individuals)', 'Lion (120 individuals)', 'Elephant (450 individuals)']
139
140# Arithmetic (__add__, __sub__)
141print("\n=== ARITHMETIC ===")
142more_lions = lion + 30 # Increase population
143print(f"lion + 30 = {more_lions}") # "Lion (150 individuals)"
144
145fewer_rhinos = rhino - 10
146print(f"rhino - 10 = {fewer_rhinos}") # "Rhino (35 individuals)"
147
148# Callable (__call__)
149print("\n=== CALLABLE - adding observations ===")
150print(lion("Serengeti", 12)) # Call like a function!
151print(lion("Masai Mara", 8))
152print(elephant("Amboseli", 35))
153
154# Container (__len__, __getitem__, __contains__)
155print("\n=== CONTAINER ===")
156print(f"Number of lion observations: {len(lion)}") # __len__
157print(f"First observation: {lion[0]}") # __getitem__
158print(f"'Serengeti' in lion? {'Serengeti' in lion}") # __contains__ - True
159
160# Iteration (through __getitem__)
161print("\nAll lion observations:")
162for obs in lion:
163 print(f" - {obs['date']} in {obs['location']}: {obs['count']} individuals")
164
165# Bool (__bool__)
166print("\n=== BOOL ===")
167print(f"bool(lion)? {bool(lion)}") # True - has population
168print(f"bool(extinct)? {bool(extinct)}") # False - population 0
169
170if lion:
171 print("Lion has a population!")
172
173if not extinct:
174 print("Dodo is extinct...")
175
176# Hash (__hash__) - works in set and dict
177print("\n=== HASH - usage in set/dict ===")
178species_set = {lion, elephant, rhino}
179print(f"Species set: {len(species_set)} elements")
180
181species_dict = {
182 lion: "Predator",
183 elephant: "Herbivore",
184 rhino: "Herbivore"
185}
186print(f"Lion type: {species_dict[lion]}")List of key magic methods
Initialization and representation
__init__(self, ...)- constructor__new__(cls, ...)- creates the object; Python calls it before__init__(rarely overridden)__str__(self)- string for the user (print)__repr__(self)- string for the programmer (repr)__format__(self, format_spec)- formatting (f"{obj:spec}")
Comparisons
__eq__(self, other)- equality==__ne__(self, other)- inequality!=__lt__(self, other)- less than<__le__(self, other)- less than or equal<=__gt__(self, other)- greater than>__ge__(self, other)- greater than or equal>=
Arithmetic
__add__(self, other)- addition+__sub__(self, other)- subtraction-__mul__(self, other)- multiplication*__truediv__(self, other)- division/__floordiv__(self, other)- floor division//__mod__(self, other)- modulo%__pow__(self, other)- exponentiation**
Container
__len__(self)- lengthlen()__getitem__(self, key)- readobj[key]__setitem__(self, key, value)- writeobj[key] = value__delitem__(self, key)- deletedel obj[key]__contains__(self, item)- membershipitem in obj__iter__(self)- iteratorfor x in obj__next__(self)- next element
Callable and Context Manager
__call__(self, ...)- callobj()__enter__(self)- enterwith__exit__(self, ...)- exitwith
Other
__bool__(self)- conversion to bool__hash__(self)- hash for set/dict__del__(self)- destructor (rarely used)
Summary
In this lesson you learned:
- What magic methods (dunder methods) are
- How to make classes behave like built-in types
- Implementing
__str__,__repr__,__eq__,__lt__ - Arithmetic operators (
__add__,__sub__,__mul__) - Container behaviors (
__len__,__getitem__,__contains__) - Callable objects with
__call__ - Context managers with
__enter__and__exit__ - Practical example with a complete cataloging system
Checkpoint
Before moving on:
- You understand the difference between
__str__and__repr__ - You can implement comparisons
- You know how to add arithmetic operators
- You understand
__len__,__getitem__,__iter__ - You can create a callable object with
__call__
Safari Analogy: Magic methods are animal instincts - a lion doesn't learn to roar, it just does it! Your classes can have natural behaviors too!
In the next lesson Darwin will teach you type hints - how to precisely describe data types so your code is more readable and safe!
Spotted a mistake in this lesson?
Check yourself
Answer the questions from this lesson. Pick an answer to see right away whether it is correct.
1. What is the difference between __str__ and __repr__?
2. When is the __len__ method called?
These are 2 of 3 questions for this lesson. Solve the rest in the game.
Hands-on tasks in the game
- Vertical ordering
Arrange the object lifecycle:
- Code editor
Add a __str__ method to the Animal class that returns 'Animal: {name}'
- Code editor
In the Animal(name, species) class, add an __eq__ method that compares animals by species (same species = equal)
- Click in order
Click elements in the correct order:
- Click in order
Click the parts of the header of the method that print(object) calls: