Inheritance and super()
Inheritance is the core mechanism of object-oriented programming that reuses and extends the attributes and methods of an existing class. It increases code reusability and clearly expresses "is-a" relationships.
Inheritance Basics
class Animal:
"""Base class (parent class, superclass)"""
def __init__(self, name: str, age: int):
self.name = name
self.age = age
def eat(self) -> str:
return f"{self.name} is eating."
def sleep(self) -> str:
return f"{self.name} is sleeping."
def describe(self) -> str:
return f"{self.name} (age: {self.age})"
def __repr__(self) -> str:
return f"{self.__class__.__name__}(name={self.name!r}, age={self.age})"
class Dog(Animal):
"""Derived class (child class, subclass)"""
def __init__(self, name: str, age: int, breed: str):
super().__init__(name, age) # Call parent's __init__
self.breed = breed
def bark(self) -> str:
return f"{self.name}: Woof!"
def fetch(self) -> str:
return f"{self.name} fetches the ball."
def describe(self) -> str:
"""Method overriding: redefine the parent method"""
base = super().describe() # Call parent method
return f"{base}, breed: {self.breed}"
class Cat(Animal):
def __init__(self, name: str, age: int, indoor: bool = True):
super().__init__(name, age)
self.indoor = indoor
def meow(self) -> str:
return f"{self.name}: Meow~"
def describe(self) -> str:
indoor_str = "indoor" if self.indoor else "outdoor"
return f"{super().describe()}, {indoor_str} cat"
# Usage examples
dog = Dog("Buddy", 3, "Jindo")
cat = Cat("Nabi", 2)
print(dog.eat()) # Buddy is eating. (inherited parent method)
print(dog.bark()) # Buddy: Woof! (child method)
print(dog.describe()) # Buddy (age: 3), breed: Jindo
print(cat.meow()) # Nabi: Meow~
print(cat.describe()) # Nabi (age: 2), indoor cat
# isinstance and issubclass
print(isinstance(dog, Dog)) # True
print(isinstance(dog, Animal)) # True (inheritance relationship)
print(issubclass(Dog, Animal)) # True
print(issubclass(Cat, Dog)) # False
How to Use super()
super() returns a proxy object that references the parent class. It references the next class in the MRO (Method Resolution Order).
class Vehicle:
def __init__(self, brand: str, model: str, year: int):
self.brand = brand
self.model = model
self.year = year
self._is_running = False
def start(self) -> str:
self._is_running = True
return f"{self.brand} {self.model} engine started."
def stop(self) -> str:
self._is_running = False
return f"{self.brand} {self.model} engine stopped."
def status(self) -> str:
state = "running" if self._is_running else "stopped"
return f"{self.brand} {self.model} ({self.year}) — {state}"
class ElectricVehicle(Vehicle):
def __init__(self, brand: str, model: str, year: int, battery_kwh: float):
super().__init__(brand, model, year) # Parent initialization
self.battery_kwh = battery_kwh
self._charge_level = 100.0 # percent
def start(self) -> str:
if self._charge_level < 5:
return "Insufficient charge. Please charge first."
result = super().start() # Use parent method result
return f"{result} (Battery: {self._charge_level:.0f}%)"
def charge(self, kwh: float) -> str:
max_charge = self.battery_kwh - (self._charge_level / 100 * self.battery_kwh)
actual_charge = min(kwh, max_charge)
self._charge_level = min(100.0, self._charge_level + (actual_charge / self.battery_kwh * 100))
return f"Charged {actual_charge:.1f} kWh. Current: {self._charge_level:.0f}%"
def status(self) -> str:
base = super().status()
return f"{base} | Battery: {self._charge_level:.0f}%"
class HybridVehicle(ElectricVehicle):
def __init__(self, brand: str, model: str, year: int, battery_kwh: float, fuel_liters: float):
super().__init__(brand, model, year, battery_kwh)
self.fuel_liters = fuel_liters
self._fuel_level = 100.0
def status(self) -> str:
base = super().status()
return f"{base} | Fuel: {self._fuel_level:.0f}%"
ev = ElectricVehicle("Tesla", "Model 3", 2024, 75.0)
print(ev.start()) # Tesla Model 3 engine started. (Battery: 100%)
print(ev.charge(20.0)) # Charged 0.0 kWh. Current: 100%
print(ev.status())
hybrid = HybridVehicle("Toyota", "Prius", 2024, 8.8, 43.0)
print(hybrid.status())
Method Overriding
class Shape:
def __init__(self, color: str = "black"):
self.color = color
def area(self) -> float:
raise NotImplementedError(f"{self.__class__.__name__}.area() must be implemented.")
def perimeter(self) -> float:
raise NotImplementedError(f"{self.__class__.__name__}.perimeter() must be implemented.")
def describe(self) -> str:
return (f"{self.__class__.__name__} — "
f"color: {self.color}, area: {self.area():.2f}, perimeter: {self.perimeter():.2f}")
class Circle(Shape):
def __init__(self, radius: float, color: str = "black"):
super().__init__(color)
self.radius = radius
def area(self) -> float:
import math
return math.pi * self.radius ** 2
def perimeter(self) -> float:
import math
return 2 * math.pi * self.radius
class Rectangle(Shape):
def __init__(self, width: float, height: float, color: str = "black"):
super().__init__(color)
self.width = width
self.height = height
def area(self) -> float:
return self.width * self.height
def perimeter(self) -> float:
return 2 * (self.width + self.height)
class Triangle(Shape):
def __init__(self, a: float, b: float, c: float, color: str = "black"):
super().__init__(color)
if a + b <= c or a + c <= b or b + c <= a:
raise ValueError("Triangle inequality is not satisfied.")
self.a, self.b, self.c = a, b, c
def area(self) -> float:
s = self.perimeter() / 2
return (s * (s - self.a) * (s - self.b) * (s - self.c)) ** 0.5
def perimeter(self) -> float:
return self.a + self.b + self.c
shapes: list[Shape] = [
Circle(5, "red"),
Rectangle(4, 6, "blue"),
Triangle(3, 4, 5, "green"),
]
for shape in shapes:
print(shape.describe())
total_area = sum(s.area() for s in shapes)
print(f"\nTotal area: {total_area:.2f}")
Multiple Inheritance
Python supports multiple inheritance — a single class can inherit from multiple parent classes.
class Flyable:
def fly(self) -> str:
return f"{self.__class__.__name__} is flying."
def altitude(self) -> str:
return "Altitude: unknown"
class Swimmable:
def swim(self) -> str:
return f"{self.__class__.__name__} is swimming."
def depth(self) -> str:
return "Depth: unknown"
class Walkable:
def walk(self) -> str:
return f"{self.__class__.__name__} is walking."
class Duck(Flyable, Swimmable, Walkable):
"""Duck: can fly, swim, and walk"""
def __init__(self, name: str):
self.name = name
def quack(self) -> str:
return f"{self.name}: Quack!"
def fly(self) -> str: # Override
return f"{self.name} flies a short distance."
donald = Duck("Donald")
print(donald.fly()) # Donald flies a short distance.
print(donald.swim()) # Duck is swimming.
print(donald.walk()) # Duck is walking.
print(donald.quack()) # Donald: Quack!
MRO (Method Resolution Order)
Python uses the C3 linearization algorithm to determine the order in which methods are searched during multiple inheritance.
class A:
def method(self) -> str:
return "A"
class B(A):
def method(self) -> str:
return f"B → {super().method()}"
class C(A):
def method(self) -> str:
return f"C → {super().method()}"
class D(B, C):
def method(self) -> str:
return f"D → {super().method()}"
d = D()
print(d.method()) # D → B → C → A
# Check MRO
print(D.__mro__)
# (<class 'D'>, <class 'B'>, <class 'C'>, <class 'A'>, <class 'object'>)
# Also available via mro() method
print(D.mro())
Diamond Inheritance Problem and Solution
class Base:
def __init__(self):
print("Base.__init__")
self.base_value = "base"
class Left(Base):
def __init__(self):
print("Left.__init__")
super().__init__()
self.left_value = "left"
class Right(Base):
def __init__(self):
print("Right.__init__")
super().__init__()
self.right_value = "right"
class Diamond(Left, Right):
def __init__(self):
print("Diamond.__init__")
super().__init__() # MRO calls Left → Right → Base in order
self.diamond_value = "diamond"
# Base.__init__ is called exactly once (thanks to super())
d = Diamond()
# Output:
# Diamond.__init__
# Left.__init__
# Right.__init__
# Base.__init__
print(d.base_value) # base
print(d.left_value) # left
print(d.right_value) # right
print(d.diamond_value) # diamond
Correct Combination of super() and Multiple Inheritance
class LogMixin:
"""Mixin that adds logging functionality"""
def __init__(self, *args, **kwargs):
super().__init__(*args, **kwargs)
self._log: list[str] = []
def log(self, message: str) -> None:
import datetime
timestamp = datetime.datetime.now().strftime("%H:%M:%S")
self._log.append(f"[{timestamp}] {message}")
def get_logs(self) -> list[str]:
return list(self._log)
class TimestampMixin:
"""Mixin that adds creation/modification timestamps"""
def __init__(self, *args, **kwargs):
super().__init__(*args, **kwargs)
import datetime
self.created_at = datetime.datetime.now()
self.updated_at = self.created_at
def touch(self) -> None:
import datetime
self.updated_at = datetime.datetime.now()
class BaseModel:
def __init__(self, id: int, name: str):
self.id = id
self.name = name
def __repr__(self) -> str:
return f"{self.__class__.__name__}(id={self.id}, name={self.name!r})"
class User(LogMixin, TimestampMixin, BaseModel):
def __init__(self, id: int, name: str, email: str):
super().__init__(id=id, name=name)
self.email = email
def update_email(self, new_email: str) -> None:
old = self.email
self.email = new_email
self.touch()
self.log(f"Email changed: {old} → {new_email}")
user = User(1, "Alice", "alice@example.com")
user.update_email("alice.new@example.com")
user.log("Profile viewed")
print(user)
print(f"Created: {user.created_at.strftime('%H:%M:%S')}")
print("Logs:", user.get_logs())
print("MRO:", [c.__name__ for c in User.__mro__])
is-a Relationship Design Principles
# Correct inheritance — is-a relationship
class Employee:
def __init__(self, name: str, salary: float):
self.name = name
self.salary = salary
def work(self) -> str:
return f"{self.name} is working."
def get_pay(self) -> float:
return self.salary
class Manager(Employee): # Manager IS-A Employee ✅
def __init__(self, name: str, salary: float, department: str):
super().__init__(name, salary)
self.department = department
self._team: list[Employee] = []
def add_to_team(self, employee: Employee) -> None:
self._team.append(employee)
def get_pay(self) -> float:
bonus = self.salary * 0.2 # Manager bonus 20%
return self.salary + bonus
def team_size(self) -> int:
return len(self._team)
class Engineer(Employee): # Engineer IS-A Employee ✅
def __init__(self, name: str, salary: float, tech_stack: list[str]):
super().__init__(name, salary)
self.tech_stack = tech_stack
def work(self) -> str:
techs = ", ".join(self.tech_stack)
return f"{self.name} is developing with {techs}."
# Using polymorphism
team: list[Employee] = [
Engineer("Alice", 5_000_000, ["Python", "FastAPI"]),
Engineer("Bob", 4_500_000, ["Java", "Spring"]),
Manager("Carol", 7_000_000, "Backend"),
]
total_payroll = sum(e.get_pay() for e in team)
print(f"Total payroll: {total_payroll:,.0f}")
for emp in team:
print(f" {emp.work()} — pay: {emp.get_pay():,.0f}")
Practical Example: Employee System
from abc import ABC, abstractmethod
from datetime import date
class Employee(ABC):
"""Abstract base class for employees"""
def __init__(self, emp_id: str, name: str, hire_date: date):
self.emp_id = emp_id
self.name = name
self.hire_date = hire_date
@abstractmethod
def calculate_pay(self) -> float:
"""Pay calculation — must be implemented by subclasses"""
...
@property
def years_of_service(self) -> float:
delta = date.today() - self.hire_date
return delta.days / 365.25
def get_info(self) -> str:
return (f"ID: {self.emp_id} | {self.name} | "
f"Hired: {self.hire_date} ({self.years_of_service:.1f} years)")
class FullTimeEmployee(Employee):
def __init__(self, emp_id: str, name: str, hire_date: date,
monthly_salary: float):
super().__init__(emp_id, name, hire_date)
self.monthly_salary = monthly_salary
def calculate_pay(self) -> float:
base = self.monthly_salary
# Seniority bonus: 5% for every 5 years
bonus_rate = (int(self.years_of_service) // 5) * 0.05
return base * (1 + bonus_rate)
class PartTimeEmployee(Employee):
def __init__(self, emp_id: str, name: str, hire_date: date,
hourly_rate: float, hours_worked: float):
super().__init__(emp_id, name, hire_date)
self.hourly_rate = hourly_rate
self.hours_worked = hours_worked
def calculate_pay(self) -> float:
overtime = max(0, self.hours_worked - 160) # 1.5x over 160 hours
regular_pay = min(self.hours_worked, 160) * self.hourly_rate
overtime_pay = overtime * self.hourly_rate * 1.5
return regular_pay + overtime_pay
class Contractor(Employee):
def __init__(self, emp_id: str, name: str, hire_date: date,
project_fee: float, completion_rate: float):
super().__init__(emp_id, name, hire_date)
self.project_fee = project_fee
self.completion_rate = min(1.0, max(0.0, completion_rate))
def calculate_pay(self) -> float:
return self.project_fee * self.completion_rate
employees: list[Employee] = [
FullTimeEmployee("E001", "Alice", date(2015, 3, 1), 4_500_000),
FullTimeEmployee("E002", "Bob", date(2010, 7, 15), 6_000_000),
PartTimeEmployee("P001", "Carol", date(2024, 1, 10), 12_000, 120),
Contractor("C001", "Dave", date(2024, 2, 1), 10_000_000, 0.75),
]
print("=== Payroll Statement ===")
total = 0
for emp in employees:
pay = emp.calculate_pay()
total += pay
print(f"{emp.get_info()}")
print(f" → Monthly pay: {pay:,.0f}")
print(f"\nTotal payroll: {total:,.0f}")
Expert Tips
1. Mixin Classes — Combining Features
class SerializeMixin:
"""Mixin that adds JSON serialization"""
def to_dict(self) -> dict:
return {k: v for k, v in vars(self).items() if not k.startswith("_")}
def to_json(self) -> str:
import json
return json.dumps(self.to_dict(), ensure_ascii=False, default=str)
class ValidateMixin:
"""Mixin that adds validation"""
def validate(self) -> list[str]:
errors = []
for attr, value in vars(self).items():
if attr.startswith("_"):
continue
if value is None:
errors.append(f"{attr} cannot be None.")
return errors
def is_valid(self) -> bool:
return len(self.validate()) == 0
class Product(SerializeMixin, ValidateMixin):
def __init__(self, name: str, price: float):
self.name = name
self.price = price
p = Product("Python Book", 35000)
print(p.to_json()) # {"name": "Python Book", "price": 35000}
print(p.is_valid()) # True
2. Auto-registering Subclasses with __init_subclass__
class Command:
_registry: dict[str, type] = {}
def __init_subclass__(cls, command_name: str = "", **kwargs):
super().__init_subclass__(**kwargs)
if command_name:
Command._registry[command_name] = cls
def execute(self) -> str:
raise NotImplementedError
@classmethod
def dispatch(cls, name: str) -> "Command":
if name not in cls._registry:
raise KeyError(f"Unknown command: {name}")
return cls._registry[name]()
class HelpCommand(Command, command_name="help"):
def execute(self) -> str:
return "Usage: help, quit, version"
class QuitCommand(Command, command_name="quit"):
def execute(self) -> str:
return "Quitting."
for cmd_name in ["help", "quit"]:
cmd = Command.dispatch(cmd_name)
print(cmd.execute())
Summary
- Inheritance:
class Child(Parent)— use only for is-a relationships - super(): references the next class per MRO, safe in multiple inheritance
- Overriding: child class redefines a parent method
- Multiple inheritance: inheriting from multiple parents, useful for Mixin patterns
- MRO: C3 linearization algorithm determines method lookup order
Inheritance is powerful but overuse leads to complex code. Remember the principle of "Composition over Inheritance."