AiTechWorlds
AiTechWorlds
Every company has a hierarchy. At the top sits the CEO — responsible for strategy, stakeholder relations, and overall company direction. Below the CEO is the CTO, who inherits all those general leadership responsibilities and adds specific technical ones: overseeing the engineering teams, choosing the tech stack, driving architectural decisions.
Further down is the Software Engineer, who inherits from the technical leadership role: they still attend meetings, still communicate with stakeholders at some level — but their day-to-day is writing and reviewing code.
Each level in the chart inherits from the one above it. Nobody re-defines what "showing up to work" means at every level — they inherit it and, where needed, extend or override it.
That is exactly how inheritance works in OOP.
Imagine building a fleet of vehicles without inheritance:
class Car:
def start(self): print("Car starts")
def stop(self): print("Car stops")
def honk(self): print("Beep!")
class Truck:
def start(self): print("Truck starts") # Copy-paste
def stop(self): print("Truck stops") # Copy-paste
def honk(self): print("HOOOONK!")
Every shared behaviour is duplicated. Fix a bug in start() and you must fix it in every class. Inheritance solves this by putting shared behaviour in one place.
class Parent:
pass
class Child(Parent): # Child inherits from Parent
pass
The child class gains everything the parent defined — all attributes, all methods — automatically.
# Level 1 — the base class
class Vehicle:
def __init__(self, make, model, year):
self.make = make
self.model = model
self.year = year
def start(self):
print(f"{self.make} {self.model} engine starting...")
def stop(self):
print(f"{self.make} {self.model} stopping.")
def __str__(self):
return f"{self.year} {self.make} {self.model}"
# Level 2 — Car inherits from Vehicle
class Car(Vehicle):
def __init__(self, make, model, year, num_doors):
super().__init__(make, model, year) # Call parent's __init__
self.num_doors = num_doors # Add Car-specific data
def honk(self):
print("Beep beep!")
def __str__(self):
# Extend parent's __str__ rather than replace it entirely
base = super().__str__()
return f"{base} ({self.num_doors}-door)"
# Level 3 — ElectricCar inherits from Car
class ElectricCar(Car):
def __init__(self, make, model, year, num_doors, battery_kwh):
super().__init__(make, model, year, num_doors) # Call Car's __init__
self.battery_kwh = battery_kwh # Add EV-specific data
def start(self):
# OVERRIDE Vehicle's start — EV doesn't have an engine noise
print(f"{self.make} {self.model} silently powering up...")
def charge(self):
print(f"Charging {self.battery_kwh} kWh battery.")
def __str__(self):
base = super().__str__()
return f"{base} [EV, {self.battery_kwh} kWh]"
# Create objects at each level
van = Vehicle("Ford", "Transit", 2020)
sedan = Car("Honda", "Accord", 2022, 4)
ev = ElectricCar("Tesla", "Model S", 2024, 4, 100)
# Vehicle behaviour
van.start()
van.stop()
print(van)
print("---")
# Car inherits start/stop, adds honk
sedan.start()
sedan.honk()
print(sedan)
print("---")
# ElectricCar OVERRIDES start, inherits stop and honk
ev.start()
ev.stop()
ev.honk()
ev.charge()
print(ev)
Output:
Ford Transit engine starting...
Ford Transit stopping.
2020 Ford Transit
---
Honda Accord engine starting...
Beep beep!
2022 Honda Accord (4-door)
---
Tesla Model S silently powering up...
Tesla Model S stopping.
Beep beep!
Charging 100 kWh battery.
2024 Tesla Model S (4-door) [EV, 100 kWh]
When ElectricCar extends Car which extends Vehicle:
ElectricCar can call stop() — defined on Vehicle, inherited through Car.ElectricCar can call honk() — defined on Car, inherited directly.ElectricCar overrides start() — it defines its own version, which shadows the one on Vehicle.You can verify the full chain with isinstance():
print(isinstance(ev, ElectricCar)) # True
print(isinstance(ev, Car)) # True
print(isinstance(ev, Vehicle)) # True
super() Functionsuper() is how you call the parent class's version of a method. It is most critical in __init__:
class Car(Vehicle):
def __init__(self, make, model, year, num_doors):
super().__init__(make, model, year) # Run Vehicle's __init__ first
self.num_doors = num_doors # Then add Car's own setup
Without super().__init__(...):
self.make, self.model, and self.year would never be set.Always call
super().__init__()in a child class unless you have a specific reason not to.
Overriding is simply defining a method in the child with the same name as one in the parent. The child's version takes precedence for that class.
You have three choices when overriding:
super().super().method() first (or last), then add new behaviour.super().method() only under certain conditions.class ElectricCar(Car):
def start(self):
# Full replacement — EV behaviour is completely different
print(f"{self.make} {self.model} silently powering up...")
Inheritance creates tight coupling — if the parent class changes, every child class may break. Prefer inheritance only when the relationship is genuinely "is-a":
| Situation | Right choice |
|---|---|
ElectricCar is a Car | Inheritance — correct |
Car has an Engine | Composition — use an attribute, not inheritance |
Logger wants to add logging to Service | Mixin or composition |
| You want to share one method across unrelated classes | Module-level function or mixin |
"Favour composition over inheritance" is one of the classic principles of OOP design. Inheritance is powerful — but composition (giving an object a reference to another object) is often more flexible and less fragile.
class Child(Parent): syntax to declare inheritance.super().__init__(...) in the child's __init__ to properly initialise the parent portion.super().method() to extend rather than fully replace.Get this course's notes on Telegram!
Free cheat sheets, summaries & practice exercises