Duck Typing
"If it walks like a duck and quacks like a duck, then it must be a duck." Python does not care about an object's class. It only cares whether the object has the methods and attributes you try to use. This philosophy enables flexible, interface-based programming.
Behavior Over Type
Duck typing focuses on what an object can do, not what it is. If an object has a speak() method, you can call it regardless of its class. This differs from statically typed languages that check class hierarchies at compile time.
Why it matters: Duck typing enables loose coupling. Functions work with any object that has the required methods, making your code more flexible and reusable.
Duck Typing Concept
speak()
speak()
speak()
make_speak(animal)
animal.speak()
Works for ALL!
Duck Typing in Action
Write functions that accept any object with the required methods. Python will call the method at runtime without checking the class.
class Dog:
def speak(self):
return "Woof!"
class Cat:
def speak(self):
return "Meow!"
class Robot:
def speak(self):
return "Beep boop!"
def make_speak(entity):
print(entity.speak()) # Works for any object with speak()
make_speak(Dog()) # Output: Woof!
make_speak(Cat()) # Output: Meow!
make_speak(Robot()) # Output: Beep boop!
The make_speak() function does not know or care about the class of the entity. It just calls speak() and trusts the object to handle it.
Polymorphic Iteration
Duck typing shines when processing collections of different objects that share common behavior.
class Circle:
def __init__(self, radius):
self.radius = radius
def area(self):
return 3.14159 * self.radius ** 2
class Rectangle:
def __init__(self, width, height):
self.width, self.height = width, height
def area(self):
return self.width * self.height
shapes = [Circle(5), Rectangle(4, 6), Circle(3)]
total = sum(shape.area() for shape in shapes)
print(f"Total area: {total:.2f}") # Output: Total area: 126.27
Different shape classes, same area() method. The loop processes them uniformly without type checks.
Practice: Duck Typing
Task: Create Bird and Airplane classes, each with a fly() method. Write a function that calls fly() on any object passed to it.
Show Solution
class Bird:
def fly(self):
return "Flapping wings!"
class Airplane:
def fly(self):
return "Engines roaring!"
def take_off(flyer):
print(flyer.fly())
take_off(Bird()) # Output: Flapping wings!
take_off(Airplane()) # Output: Engines roaring!
Task: Create Employee and Contractor classes with a get_pay() method. Make a list of both types and calculate total payroll.
Show Solution
class Employee:
def __init__(self, salary):
self.salary = salary
def get_pay(self):
return self.salary
class Contractor:
def __init__(self, hourly, hours):
self.hourly, self.hours = hourly, hours
def get_pay(self):
return self.hourly * self.hours
workers = [Employee(5000), Contractor(50, 80), Employee(6000)]
total = sum(w.get_pay() for w in workers)
print(f"Total payroll: ${total}") # Output: Total payroll: $15000
Task: Create a StringBuffer class with write() and read() methods. Write a function that works with it the same way it would with a file.
Show Solution
class StringBuffer:
def __init__(self):
self.data = ""
def write(self, text):
self.data += text
def read(self):
return self.data
def save_greeting(output):
output.write("Hello, ")
output.write("World!")
buffer = StringBuffer()
save_greeting(buffer)
print(buffer.read()) # Output: Hello, World!
Method Overriding
Method overriding occurs when a subclass provides its own implementation of a method defined in the parent class. The child's version replaces the parent's version for objects of that child class. This is classical polymorphism through inheritance.
Method Overriding Flow
speak()
"Some sound"
speak()"Woof!"
speak()"Meow!"
dog.speak()
Dog.speak() is called, NOT Animal.speak()
Basic Method Overriding
Define a method in the child class with the same name as the parent. Python uses the most specific version (child's version) for that object.
class Animal:
def speak(self):
return "Some generic sound"
class Dog(Animal):
def speak(self):
return "Woof!"
class Cat(Animal):
def speak(self):
return "Meow!"
animals = [Animal(), Dog(), Cat()]
for animal in animals:
print(animal.speak())
# Output: Some generic sound, Woof!, Meow!
Each object uses its own class's speak() method. The Animal instance uses the base version while Dog and Cat use their overridden versions.
Using super() to Extend Behavior
Use super() to call the parent's method before or after adding your own logic. This extends rather than completely replaces the parent behavior.
class Vehicle:
def __init__(self, brand):
self.brand = brand
def start(self):
return f"{self.brand} engine starting..."
class ElectricCar(Vehicle):
def __init__(self, brand, battery_kwh):
super().__init__(brand)
self.battery = battery_kwh
def start(self):
base = super().start()
return f"{base} [Silent electric mode]"
tesla = ElectricCar("Tesla", 100)
print(tesla.start())
# Output: Tesla engine starting... [Silent electric mode]
The super().start() calls the parent version first, then the child adds its own behavior. This is the "extend and customize" pattern.
Practice: Method Overriding
Task: Create a Shape base class with describe(). Create Circle and Square that override it with specific descriptions.
Show Solution
class Shape:
def describe(self):
return "I am a shape"
class Circle(Shape):
def describe(self):
return "I am a circle with no corners"
class Square(Shape):
def describe(self):
return "I am a square with 4 equal sides"
for s in [Shape(), Circle(), Square()]:
print(s.describe())
Task: Create Logger with log(msg) that prints the message. Create TimestampLogger that adds a timestamp prefix using super().
Show Solution
from datetime import datetime
class Logger:
def log(self, msg):
print(msg)
class TimestampLogger(Logger):
def log(self, msg):
timestamp = datetime.now().strftime("%H:%M:%S")
super().log(f"[{timestamp}] {msg}")
logger = TimestampLogger()
logger.log("Server started") # [14:30:25] Server started
Task: Create Account, SavingsAccount, and PremiumSavings. Override get_interest_rate() at each level, using super() to add bonuses.
Show Solution
class Account:
def get_interest_rate(self):
return 0.01 # 1% base
class SavingsAccount(Account):
def get_interest_rate(self):
return super().get_interest_rate() + 0.02 # +2%
class PremiumSavings(SavingsAccount):
def get_interest_rate(self):
return super().get_interest_rate() + 0.015 # +1.5%
acc = PremiumSavings()
print(f"Rate: {acc.get_interest_rate():.1%}") # Rate: 4.5%
Operator Overloading
Operator overloading lets you define how operators like +, -, *, and == work with your custom objects. Python uses special "dunder" (double underscore) methods to implement this. When you write a + b, Python calls a.__add__(b).
| Operator | Method | Example | Description |
|---|---|---|---|
+ |
__add__(self, other) |
a + b | Addition |
- |
__sub__(self, other) |
a - b | Subtraction |
* |
__mul__(self, other) |
a * b | Multiplication |
== |
__eq__(self, other) |
a == b | Equality check |
< |
__lt__(self, other) |
a < b | Less than |
len() |
__len__(self) |
len(a) | Length |
Overloading Arithmetic Operators
Define __add__ to enable the + operator for your objects. Return a new object as the result.
class Vector:
def __init__(self, x, y):
self.x, self.y = x, y
def __add__(self, other):
return Vector(self.x + other.x, self.y + other.y)
def __str__(self):
return f"Vector({self.x}, {self.y})"
v1 = Vector(2, 3)
v2 = Vector(5, 7)
v3 = v1 + v2 # Calls v1.__add__(v2)
print(v3) # Output: Vector(7, 10)
The __add__ method receives self (left operand) and other (right operand). It returns a new Vector instead of modifying either original.
Overloading Comparison Operators
Define __eq__ for equality (==) and __lt__ for less-than (<). Python can derive other comparisons if you use functools.total_ordering.
class Product:
def __init__(self, name, price):
self.name, self.price = name, price
def __eq__(self, other):
return self.price == other.price
def __lt__(self, other):
return self.price < other.price
p1 = Product("Phone", 999)
p2 = Product("Tablet", 999)
p3 = Product("Watch", 399)
print(p1 == p2) # True (same price)
print(p3 < p1) # True (399 < 999)
Comparison operators let you use sorted() and min()/max() with your objects. Define __lt__ to enable sorting by price.
The __str__ and __repr__ Methods
Define __str__ for human-readable output (print) and __repr__ for unambiguous representation (debugging).
class Point:
def __init__(self, x, y):
self.x, self.y = x, y
def __str__(self):
return f"Point at ({self.x}, {self.y})"
def __repr__(self):
return f"Point({self.x}, {self.y})"
p = Point(3, 4)
print(p) # Output: Point at (3, 4) [uses __str__]
print(repr(p)) # Output: Point(3, 4) [uses __repr__]
print([p]) # Output: [Point(3, 4)] [list uses __repr__]
__repr__ should ideally return a string that could recreate the object. __str__ is for friendly display to end users.
Practice: Operator Overloading
Task: Create a Book class with title and author. Implement __str__ to return "Title by Author".
Show Solution
class Book:
def __init__(self, title, author):
self.title = title
self.author = author
def __str__(self):
return f"{self.title} by {self.author}"
book = Book("1984", "George Orwell")
print(book) # Output: 1984 by George Orwell
Task: Create a Playlist class that stores songs. Implement __len__ to return the number of songs.
Show Solution
class Playlist:
def __init__(self):
self.songs = []
def add(self, song):
self.songs.append(song)
def __len__(self):
return len(self.songs)
pl = Playlist()
pl.add("Song A")
pl.add("Song B")
print(len(pl)) # Output: 2
Task: Create a Money class with amount and currency. Implement __add__ that only adds if currencies match (raise error otherwise).
Show Solution
class Money:
def __init__(self, amount, currency):
self.amount = amount
self.currency = currency
def __add__(self, other):
if self.currency != other.currency:
raise ValueError("Currency mismatch!")
return Money(self.amount + other.amount, self.currency)
def __str__(self):
return f"{self.amount} {self.currency}"
m1 = Money(100, "USD")
m2 = Money(50, "USD")
print(m1 + m2) # Output: 150 USD
Task: Create a Student class with name and gpa. Implement comparison operators so students can be sorted by GPA.
Show Solution
class Student:
def __init__(self, name, gpa):
self.name, self.gpa = name, gpa
def __eq__(self, other):
return self.gpa == other.gpa
def __lt__(self, other):
return self.gpa < other.gpa
def __repr__(self):
return f"{self.name}({self.gpa})"
students = [Student("Alice", 3.8), Student("Bob", 3.5)]
print(sorted(students)) # [Bob(3.5), Alice(3.8)]
Task: Create a Vector3D class (x, y, z). Implement __mul__ for scalar multiplication (v * 3) and __add__ for vector addition.
Show Solution
class Vector3D:
def __init__(self, x, y, z):
self.x, self.y, self.z = x, y, z
def __add__(self, other):
return Vector3D(self.x+other.x, self.y+other.y, self.z+other.z)
def __mul__(self, scalar):
return Vector3D(self.x*scalar, self.y*scalar, self.z*scalar)
def __repr__(self):
return f"V({self.x}, {self.y}, {self.z})"
v = Vector3D(1, 2, 3)
print(v * 2) # V(2, 4, 6)
print(v + Vector3D(1, 1, 1)) # V(2, 3, 4)
Task: Create a Matrix class that stores a 2D list. Implement __getitem__ to access elements with matrix[row][col] syntax.
Show Solution
class Matrix:
def __init__(self, data):
self.data = data
def __getitem__(self, row):
return self.data[row]
def __repr__(self):
return '\n'.join(str(row) for row in self.data)
m = Matrix([[1, 2, 3], [4, 5, 6], [7, 8, 9]])
print(m[1][2]) # Output: 6
print(m[0]) # Output: [1, 2, 3]
Key Takeaways
Duck Typing is Pythonic
Python checks capabilities, not types. If an object has the method you need, it works regardless of its class hierarchy.
Override to Specialize
Child classes override parent methods to provide specialized behavior while maintaining the same interface.
Use super() to Extend
Call super().method() to run the parent's code first, then add your own logic on top of it.
Operators are Methods
The + operator calls __add__, == calls __eq__. Define these dunder methods to use operators with your objects.
__str__ vs __repr__
Use __str__ for user-friendly output and __repr__ for debugging. repr should ideally be copy-pasteable Python.
Enable Sorting with __lt__
Define __lt__ to use sorted(), min(), and max() with your objects. Python derives other comparisons from it.
Knowledge Check
Quick Quiz
Test what you've learned about Python polymorphism and magic methods