Polymorphism and Magic Methods
The last two lessons covered the "form" of classes and inheritance. This lesson covers the "spirit." Polymorphism lets objects of different classes be called in the same way — you don't need to know their specific class, just that they have the required method. Magic methods make custom classes behave as naturally as built-in types.
1. What is Polymorphism
Polymorphism means "many forms" — the same interface, different implementations:
class Dog:
def speak(self):
return "Woof!"
class Cat:
def speak(self):
return "Meow!"
class Duck:
def speak(self):
return "Quack!"
# Polymorphism: regardless of class, as long as it has a speak() method
animals = [Dog(), Cat(), Duck()]
for animal in animals:
print(animal.speak())
Output:
Woof!
Meow!
Quack!
Tip: Duck Typing: "If it walks like a duck and quacks like a duck, it's a duck." Python doesn't care about an object's type — only whether it has the method you need. This is one of Python's most flexible designs.
Example: Shape Area Calculation (Difficulty: Star-Star)
class Rectangle:
def __init__(self, width, height):
self.width = width
self.height = height
def area(self):
return self.width * self.height
class Circle:
def __init__(self, radius):
self.radius = radius
def area(self):
return 3.14159 * self.radius ** 2
class Triangle:
def __init__(self, base, height):
self.base = base
self.height = height
def area(self):
return 0.5 * self.base * self.height
# Polymorphism — regardless of shape, just calculate area
shapes = [Rectangle(3, 4), Circle(5), Triangle(6, 8)]
for shape in shapes:
print(f"Area: {shape.area():.2f}")
Output:
Area: 12.00
Area: 78.54
Area: 24.00
2. Magic Methods: Making Classes More "Pythonic"
Magic methods are special methods with double underscore prefixes and suffixes — __init__, __str__, __len__, etc. They make custom classes behave like built-in types.
__str__ and __repr__: Friendlier Printing
class Person:
def __init__(self, name, age):
self.name = name
self.age = age
def __str__(self):
"""User-friendly string representation"""
return f"{self.name} ({self.age} years old)"
def __repr__(self):
"""Developer-friendly string representation"""
return f"Person('{self.name}', {self.age})"
p = Person("Zhang San", 25)
print(p) # Zhang San (25 years old) — calls __str__
print(repr(p)) # Person('Zhang San', 25) — calls __repr__
__len__: Making Objects Support len()
class Team:
def __init__(self, members=None):
self.members = members or []
def add(self, member):
self.members.append(member)
def __len__(self):
return len(self.members)
def __str__(self):
return f"Team({', '.join(self.members)})"
team = Team()
team.add("Zhang San")
team.add("Li Si")
team.add("Wang Wu")
print(len(team)) # 3 — calls __len__
print(team) # Team(Zhang San, Li Si, Wang Wu)
__getitem__: Making Objects Support Indexing
class Playlist:
def __init__(self):
self.songs = []
def add(self, song):
self.songs.append(song)
def __getitem__(self, index):
return self.songs[index]
def __len__(self):
return len(self.songs)
playlist = Playlist()
playlist.add("Sunny Day")
playlist.add("Seven Li Fragrance")
playlist.add("Nocturne")
print(playlist[0]) # Sunny Day — calls __getitem__
print(len(playlist)) # 3
# Supports slicing and iteration
for song in playlist:
print(song, end=" ") # Sunny Day Seven Li Fragrance Nocturne
3. Common Magic Methods Reference
class Demo:
def __init__(self, value): # Constructor
self.value = value
def __str__(self): # str() / print()
return str(self.value)
def __repr__(self): # repr() / debugging
return f"Demo({self.value})"
def __len__(self): # len()
return 1
def __bool__(self): # bool() / if condition
return self.value != 0
def __eq__(self, other): # ==
return self.value == other.value
def __lt__(self, other): # < (for sorting)
return self.value < other.value
def __add__(self, other): # + operator
return Demo(self.value + other.value)
def __getitem__(self, key): # obj[key]
return key
Example: Fraction Class (Difficulty: Star-Star-Star)
class Fraction:
"""Fraction class — supports addition"""
def __init__(self, numerator, denominator=1):
if denominator == 0:
raise ValueError("Denominator cannot be zero")
self.numerator = numerator
self.denominator = denominator
def __str__(self):
if self.denominator == 1:
return str(self.numerator)
return f"{self.numerator}/{self.denominator}"
def __add__(self, other):
new_den = self.denominator * other.denominator
new_num = self.numerator * other.denominator + other.numerator * self.denominator
return Fraction(new_num, new_den)
def __eq__(self, other):
return (self.numerator * other.denominator) == (other.numerator * self.denominator)
f1 = Fraction(1, 3)
f2 = Fraction(1, 6)
print(f1) # 1/3
print(f1 + f2) # 9/18
print(Fraction(1, 2) == Fraction(2, 4)) # True
4. @dataclass: Quickly Defining Data Classes
Writing a class just for storing data, with manual __init__, __repr__, __eq__, is tedious. @dataclass generates them automatically:
from dataclasses import dataclass
@dataclass
class Point:
x: float
y: float
@dataclass
class Student:
name: str
age: int
score: float = 0.0 # Default value
# Automatically gets __init__, __repr__, __eq__
p1 = Point(3.0, 4.0)
p2 = Point(3.0, 4.0)
print(p1) # Point(x=3.0, y=4.0)
print(p1 == p2) # True (compares all fields automatically)
s = Student("Zhang San", 20, 85.5)
print(s) # Student(name='Zhang San', age=20, score=85.5)
Tip: When to use
@dataclass: 1) A class mainly stores data with few methods. 2) You need boilerplate like__init__,__repr__,__eq__. 3) You don't want to write these manually. For simple cases, it's much more efficient than hand-writing classes.
Common Use Cases
- Duck typing: Python's
len(),for...in,sorted()are all protocol-based — implement the corresponding method and your class can use them. - Data containers: Implement
__getitem__+__len__to make your class support indexing and iteration. - Operator overloading:
__add__,__eq__,__lt__let custom classes support+,==,<etc. - Data classes:
@dataclassfor quickly defining configurations, DTOs, value objects.
FAQ
print() calls __str__, len() calls __len__, == calls __eq__. You don't call them directly; just implement them, and Python calls them automatically at the right time.@dataclass and a regular class?@dataclass auto-generates the methods you're tired of writing. It's just a code generator — the generated class is identical to a hand-written one. Classes decorated with @dataclass can also have their own methods. If you find yourself repeatedly writing __init__ to store properties, it's time for @dataclass.Summary
- Polymorphism: objects of different classes with the same-named method can be called the same way
- Duck typing: "If it quacks, it's a duck" — cares about methods, not types
- Magic methods:
__str__(printing),__len__(length),__getitem__(indexing),__eq__(equality),__add__(addition), etc. @dataclassauto-generates__init__,__repr__,__eq__— ideal for pure data classes
Exercises
-
Beginner (Difficulty: Star): Define a
Bookclass implementing__str__and__len__(returning the page count). Create two books and outputprint(book)andlen(book). -
Intermediate (Difficulty: Star-Star): Use
@dataclassto define aProductclass (name, price, quantity). Implement__add__— merge two products with the same name (sum quantities, average price). Hint: Implement inside the @dataclass class. -
Advanced (Difficulty: Star-Star-Star): Define a
Deckclass (a deck of playing cards) implementing__len__,__getitem__(supporting indexing and slicing), andshuffle()(usingrandom.shuffle). Then write code to create a deck and deal the first 5 cards as a hand.



