Welcome back, @name! 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__!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"?
__init__ - ConstructorYou 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____str__ - Human-readable representationCalled by
print() and str() - returns a user-friendly string.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", 3)
11print(lion) # "Simba (age: 3 years)" - calls __str__
12print(str(lion)) # Same thing__repr__ - Programmer representationCalled 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", 3)
15
16# In Python console
17>>> lion
18Animal(name='Simba', age=3) # Calls __repr__
19
20# In print
21print(lion) # "Simba (3 years)" - calls __str__
22
23# repr() explicitly
24print(repr(lion)) # "Animal(name='Simba', age=3)"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!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!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"Make your objects behave like lists, dictionaries!
__len__ - Length1class 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 access1class 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__ - Iteration1class 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"
37# print(next(exp2)) # StopIteration__call__ - Callable objectsMakes 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"__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"💾 Saving {len(self.photos)} 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")
29# __exit__ called automatically at the end of the block!
30
31# Output:
32# 📸 Turning on camera in Serengeti
33# 📷 Photo: Lion hunting
34# 📷 Photo: Elephant herd
35# 📷 Photo: Giraffe by a tree
36# 💾 Saving 3 photos
37# 📴 Turning off camera1from 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 - so it works in set() and dict keys"""
53 return hash(self.scientific_name)
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]}")__init__(self, ...) - constructor__str__(self) - string for the user (print)__repr__(self) - string for the programmer (repr)__format__(self, format_spec) - formatting (f"{obj:spec}")__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 >=__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 **__len__(self) - length len()__getitem__(self, key) - read obj[key]__setitem__(self, key, value) - write obj[key] = value__delitem__(self, key) - delete del obj[key]__contains__(self, item) - membership item in obj__iter__(self) - iterator for x in obj__next__(self) - next element__call__(self, ...) - call obj()__enter__(self) - enter with__exit__(self, ...) - exit with__bool__(self) - conversion to bool__hash__(self) - hash for set/dict__del__(self) - destructor (rarely used)In this lesson you learned:
__str__, __repr__, __eq__, __lt____add__, __sub__, __mul__)__len__, __getitem__, __contains__)__call____enter__ and __exit__Before moving on:
__str__ and __repr____len__, __getitem__, __iter____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! 📝🔍