Single Inheritance
A child class inherits all attributes and methods from its parent and can override or extend them.
class Animal:
def __init__(self, name, sound):
self.name = name
self.sound = sound
def speak(self):
return f"{self.name} says {self.sound}!"
def __repr__(self):
return f"{type(self).__name__}({self.name!r})"
# Dog inherits from Animal
class Dog(Animal):
def __init__(self, name):
super().__init__(name, "Woof") # call parent __init__
def fetch(self, item):
return f"{self.name} fetches the {item}!"
class Cat(Animal):
def __init__(self, name):
super().__init__(name, "Meow")
# Override parent method
def speak(self):
return f"{self.name} says {self.sound}... and ignores you."
d = Dog("Buddy")
c = Cat("Whiskers")
print(d.speak()) # Buddy says Woof!
print(c.speak()) # Whiskers says Meow... and ignores you.
print(d.fetch("ball")) # Buddy fetches the ball!
# Inheritance checks
print(isinstance(d, Dog)) # True
print(isinstance(d, Animal)) # True - d is also an Animal
print(isinstance(d, Cat)) # False
print(issubclass(Dog, Animal)) # True
print(issubclass(Dog, Cat)) # False
# All classes implicitly inherit from object
print(issubclass(Animal, object)) # True
print(d.__class__.__mro__)
# (, , )
super()
super() gives you access to the parent class in the MRO chain. It is essential for cooperative multiple inheritance.
class Vehicle:
def __init__(self, make, model, year):
self.make = make
self.model = model
self.year = year
def info(self):
return f"{self.year} {self.make} {self.model}"
class Car(Vehicle):
def __init__(self, make, model, year, doors=4):
super().__init__(make, model, year) # initialize parent
self.doors = doors
def info(self):
base = super().info() # call parent method
return f"{base} ({self.doors}-door)"
class ElectricCar(Car):
def __init__(self, make, model, year, range_km, doors=4):
super().__init__(make, model, year, doors) # initialize Car
self.range_km = range_km
def info(self):
base = super().info()
return f"{base} - EV ({self.range_km}km range)"
ec = ElectricCar("Tesla", "Model 3", 2024, 490)
print(ec.info())
# 2024 Tesla Model 3 (4-door) - EV (490km range)
# super() in class methods
class Base:
@classmethod
def create(cls, **kwargs):
obj = cls.__new__(cls)
return obj
class Child(Base):
def __init__(self, x):
self.x = x
@classmethod
def create_with_default(cls):
obj = super().create() # calls Base.create
obj.x = 0
return obj
Multiple Inheritance
class Flyable:
def fly(self):
return f"{self.__class__.__name__} is flying"
class Swimmable:
def swim(self):
return f"{self.__class__.__name__} is swimming"
class Duck(Flyable, Swimmable):
def quack(self):
return "Quack!"
d = Duck()
print(d.fly()) # Duck is flying
print(d.swim()) # Duck is swimming
print(d.quack()) # Quack!
# All inherited methods are available
print(isinstance(d, Flyable)) # True
print(isinstance(d, Swimmable)) # True
# Diamond inheritance - the classic problem
class A:
def greet(self):
return "Hello from A"
class B(A):
def greet(self):
return f"B + {super().greet()}"
class C(A):
def greet(self):
return f"C + {super().greet()}"
class D(B, C):
pass
d = D()
print(d.greet()) # B + C + Hello from A
# MRO: D -> B -> C -> A
# super() in B delegates to C (not A!), ensuring A is called once
print(D.__mro__)
# (, , , , )
Method Resolution Order (MRO)
Python uses the C3 linearization algorithm to determine the order in which classes are searched for methods.
class A: pass
class B(A): pass
class C(A): pass
class D(B, C): pass
# View MRO
print(D.__mro__)
# (, , , , )
print(D.mro()) # same, returns list
# Method lookup follows MRO left to right
class A:
def method(self): return "A"
class B(A):
def method(self): return "B"
class C(A):
def method(self): return "C"
class D(B, C): pass
class E(C, B): pass # different order changes MRO
print(D().method()) # B (D -> B comes first)
print(E().method()) # C (E -> C comes first)
# Cooperative multiple inheritance - all super() calls must pass args
class Base:
def __init__(self):
super().__init__() # important: always call super().__init__()
class LogMixin:
def __init__(self):
super().__init__()
print(f"LogMixin init for {type(self).__name__}")
class TimestampMixin:
def __init__(self):
super().__init__()
import datetime
self.created_at = datetime.datetime.now()
class Service(LogMixin, TimestampMixin, Base):
def __init__(self, name):
self.name = name
super().__init__() # triggers cooperative chain
s = Service("MyService")
# LogMixin init for Service
print(s.created_at) # 2024-xx-xx ...
Abstract Classes
Abstract classes define an interface that subclasses must implement. You cannot instantiate an abstract class directly.
from abc import ABC, abstractmethod
class Shape(ABC):
"""Abstract base class for all shapes."""
@abstractmethod
def area(self) -> float:
"""Calculate area - must be implemented by subclasses."""
...
@abstractmethod
def perimeter(self) -> float:
"""Calculate perimeter - must be implemented."""
...
def describe(self):
"""Concrete method - available to all subclasses."""
return f"{type(self).__name__}: area={self.area():.2f}, perimeter={self.perimeter():.2f}"
# Cannot instantiate abstract class
try:
s = Shape() # TypeError: Can't instantiate abstract class
except TypeError as e:
print(e)
# Concrete subclass must implement ALL abstract methods
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)
class Circle(Shape):
PI = 3.14159265
def __init__(self, radius):
self.radius = radius
def area(self):
return self.PI * self.radius ** 2
def perimeter(self):
return 2 * self.PI * self.radius
r = Rectangle(4, 5)
c = Circle(3)
print(r.describe()) # Rectangle: area=20.00, perimeter=18.00
print(c.describe()) # Circle: area=28.27, perimeter=18.85
# Polymorphism - treat all shapes uniformly
shapes = [Rectangle(4, 5), Circle(3), Rectangle(2, 8)]
total_area = sum(s.area() for s in shapes)
print(f"Total area: {total_area:.2f}")
# Abstract class with some concrete methods
class DataProcessor(ABC):
@abstractmethod
def fetch(self) -> list:
...
@abstractmethod
def transform(self, data: list) -> list:
...
def process(self): # template method pattern
data = self.fetch()
return self.transform(data)
Mixins
Mixins are small, focused classes that add a specific capability through multiple inheritance without being a base class.
import json
# Mixin: adds JSON serialization capability
class JsonMixin:
def to_json(self):
return json.dumps(self.__dict__, default=str)
@classmethod
def from_json(cls, json_str):
data = json.loads(json_str)
return cls(**data)
# Mixin: adds comparison by a key
class ComparableMixin:
def _compare_key(self):
raise NotImplementedError
def __lt__(self, other): return self._compare_key() < other._compare_key()
def __le__(self, other): return self._compare_key() <= other._compare_key()
def __eq__(self, other): return self._compare_key() == other._compare_key()
def __gt__(self, other): return self._compare_key() > other._compare_key()
# Mixin: adds repr based on __dict__
class ReprMixin:
def __repr__(self):
attrs = ", ".join(f"{k}={v!r}" for k, v in self.__dict__.items())
return f"{type(self).__name__}({attrs})"
# Primary class using mixins
class Product(JsonMixin, ComparableMixin, ReprMixin):
def __init__(self, name, price, stock):
self.name = name
self.price = price
self.stock = stock
def _compare_key(self):
return self.price
p1 = Product("Widget", 9.99, 100)
p2 = Product("Gadget", 24.99, 50)
p3 = Product("Donut", 1.99, 200)
print(repr(p1)) # Product(name='Widget', price=9.99, stock=100)
print(p1.to_json()) # {"name": "Widget", "price": 9.99, "stock": 100}
products = [p1, p2, p3]
for p in sorted(products): # uses ComparableMixin
print(f"{p.name}: ${p.price}")
By convention, mixin class names end in Mixin to signal that they are not meant to stand alone. Mixins should not define __init__ (or always call super().__init__()), should not inherit from the primary class hierarchy, and should be focused on one small capability. This keeps multiple inheritance manageable.