Single Inheritance
Basic Inheritance
class Animal:
def __init__(self, name):
self.name = name
def speak(self):
raise NotImplementedError
def __str__(self):
return f'{self.name} the {self.__class__.__name__}'
class Dog(Animal):
def speak(self):
return f'{self.name} says Woof!'
def fetch(self, item):
return f'{self.name} fetches the {item}'
class Cat(Animal):
def speak(self):
return f'{self.name} says Meow!'
dog = Dog('Buddy')
cat = Cat('Whiskers')
print(dog) # Buddy the Dog
print(dog.speak()) # Buddy says Woof!
print(dog.fetch('ball')) # Buddy fetches the ball
print(cat.speak()) # Whiskers says Meow!
Using super()
class Person:
def __init__(self, name, age):
self.name = name
self.age = age
def __str__(self):
return f'{self.name}, {self.age} years old'
class Student(Person):
def __init__(self, name, age, major):
super().__init__(name, age) # Call parent __init__
self.major = major
def __str__(self):
return f'{super().__str__()}, studying {self.major}'
student = Student('Alice', 20, 'Computer Science')
print(student) # Alice, 20 years old, studying Computer Science
isinstance() and issubclass()
dog = Dog('Buddy')
isinstance(dog, Dog) # True
isinstance(dog, Animal) # True
isinstance(dog, Cat) # False
issubclass(Dog, Animal) # True
issubclass(Animal, Dog) # False
issubclass(Dog, object) # True (everything inherits from object)
Multiple Inheritance
Multiple Inheritance
class Flyer:
def fly(self):
return f'{self.__class__.__name__} is flying'
class Swimmer:
def swim(self):
return f'{self.__class__.__name__} is swimming'
class Duck(Animal, Flyer, Swimmer):
def speak(self):
return f'{self.name} says Quack!'
duck = Duck('Donald')
print(duck.speak()) # Donald says Quack!
print(duck.fly()) # Duck is flying
print(duck.swim()) # Duck is swimming
Method Resolution Order (MRO)
class A:
def greet(self):
return 'Hello from A'
class B(A):
def greet(self):
return 'Hello from B'
class C(A):
def greet(self):
return 'Hello from C'
class D(B, C):
pass
d = D()
print(d.greet()) # Hello from B
# Check MRO
print(D.__mro__)
# (<class 'D'>, <class 'B'>, <class 'C'>, <class 'A'>, <class 'object'>)
# Or using method
print(D.mro())
super() with Multiple Inheritance
class Base:
def __init__(self):
print('Base.__init__')
class Left(Base):
def __init__(self):
super().__init__()
print('Left.__init__')
class Right(Base):
def __init__(self):
super().__init__()
print('Right.__init__')
class Child(Left, Right):
def __init__(self):
super().__init__()
print('Child.__init__')
Child()
# Output:
# Base.__init__
# Right.__init__
# Left.__init__
# Child.__init__
Common Pitfalls
# ❌ The Diamond Problem (Python handles it with MRO)
class A:
pass
class B(A):
pass
class C(A):
pass
class D(B, C): # Both B and C inherit from A
pass
# ✅ Python uses C3 linearization (MRO)
print(D.__mro__) # D -> B -> C -> A -> object
Polymorphism
Duck Typing
# "If it walks like a duck and quacks like a duck, it's a duck"
def make_it_speak(animal):
print(animal.speak()) # No type checking needed
class Dog:
def speak(self):
return 'Woof!'
class Cat:
def speak(self):
return 'Meow!'
class Duck:
def speak(self):
return 'Quack!'
# All work with the same function
for animal in [Dog(), Cat(), Duck()]:
make_it_speak(animal)
Abstract Base Classes
from abc import ABC, abstractmethod
class Shape(ABC):
@abstractmethod
def area(self):
pass
@abstractmethod
def perimeter(self):
pass
def description(self):
return f'{self.__class__.__name__} with area {self.area():.2f}'
class Circle(Shape):
def __init__(self, radius):
self.radius = radius
def area(self):
import math
return math.pi * self.radius ** 2
def perimeter(self):
import math
return 2 * math.pi * self.radius
class Rectangle(Shape):
def __init__(self, width, height):
self.width = width
self.height = height
def area(self):
return self.width * self.height
def perimeter(self):
return 2 * (self.width + self.height)
# shape = Shape() # ❌ TypeError: Can't instantiate abstract class
circle = Circle(5) # ✅
rect = Rectangle(4, 6) # ✅
shapes = [circle, rect]
for shape in shapes:
print(shape.description())
Polymorphism in Practice
def total_area(shapes):
return sum(shape.area() for shape in shapes)
def print_shapes(shapes):
for shape in shapes:
print(f'{shape.__class__.__name__}: {shape.area():.2f}')
shapes = [Circle(5), Rectangle(4, 6), Circle(3)]
print(f'Total area: {total_area(shapes):.2f}')
print_shapes(shapes)
Polymorphism Benefits
- Flexibility: Code works with any type that implements the interface
- Extensibility: Add new types without modifying existing code
- Testability: Easy to mock and test
- Loose coupling: Components don't depend on concrete types