What is Inheritance?
Inheritance lets you create a new class based on an existing one. The new class (child) inherits all attributes and methods from the existing class (parent), and can add or override them.
# Parent class (base class)
class Animal:
def __init__(self, name, sound):
self.name = name
self.sound = sound
def speak(self):
print(f"{self.name} says: {self.sound}!")
def info(self):
print(f"I'm {self.name}, an animal.")
# Child class (inherits from Animal)
class Dog(Animal):
def fetch(self, item):
print(f"{self.name} fetches the {item}!")
# Dog inherits __init__, speak, and info from Animal
rex = Dog("Rex", "Woof")
rex.speak() # Rex says: Woof!
rex.info() # I'm Rex, an animal.
rex.fetch("ball") # Rex fetches the ball!
Overriding Methods
Child classes can replace parent methods with their own version:
class Animal:
def __init__(self, name):
self.name = name
def speak(self):
print(f"{self.name} makes a sound.")
def info(self):
return f"{self.name} (Animal)"
class Cat(Animal):
def speak(self): # Override parent method
print(f"{self.name} says: Meow!")
def info(self): # Override parent method
return f"{self.name} (Cat)"
cat = Cat("Whiskers")
cat.speak() # Whiskers says: Meow! (uses Cat's version)
print(cat.info()) # Whiskers (Cat)
Using super()
super() calls the parent class's method, letting you extend rather
than completely replace it:
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 ElectricCar(Vehicle):
def __init__(self, make, model, year, battery_kwh):
super().__init__(make, model, year) # Call parent's __init__
self.battery_kwh = battery_kwh # Add new attribute
def info(self):
base = super().info() # Get parent's info
return f"{base} (Electric, {self.battery_kwh}kWh)"
tesla = ElectricCar("Tesla", "Model 3", 2024, 75)
print(tesla.info()) # 2024 Tesla Model 3 (Electric, 75kWh)
When your child class has its own __init__, always call
super().__init__(...) first to ensure the parent is properly
initialized. Forgetting this is a common source of bugs.
Polymorphism
Polymorphism means different classes can be used through the same interface. If multiple classes have the same method name, you can call it without knowing which class the object belongs to:
class Shape:
def area(self):
raise NotImplementedError("Subclasses must implement area()")
class Rectangle(Shape):
def __init__(self, width, height):
self.width = width
self.height = height
def area(self):
return self.width * self.height
class Circle(Shape):
def __init__(self, radius):
self.radius = radius
def area(self):
import math
return math.pi * self.radius ** 2
class Triangle(Shape):
def __init__(self, base, height):
self.base = base
self.height = height
def area(self):
return 0.5 * self.base * self.height
# Polymorphism in action — same method, different behavior
shapes = [
Rectangle(10, 5),
Circle(7),
Triangle(8, 6)
]
for shape in shapes:
print(f"{type(shape).__name__}: area = {shape.area():.2f}")
Checking Inheritance
class Animal: pass
class Dog(Animal): pass
class Cat(Animal): pass
rex = Dog()
print(isinstance(rex, Dog)) # True
print(isinstance(rex, Animal)) # True (Dog inherits from Animal)
print(isinstance(rex, Cat)) # False
print(issubclass(Dog, Animal)) # True
print(issubclass(Cat, Dog)) # False
Composition vs Inheritance
Not everything should use inheritance. Sometimes it's better for a class to contain another class (composition) rather than inherit from it:
# Inheritance: "is a" relationship
# A Dog IS an Animal — inheritance makes sense
class Dog(Animal): pass
# Composition: "has a" relationship
# A Car HAS an Engine — composition makes sense
class Engine:
def __init__(self, horsepower):
self.horsepower = horsepower
def start(self):
print(f"Engine ({self.horsepower}hp) started!")
class Car:
def __init__(self, make, engine):
self.make = make
self.engine = engine # Car HAS an Engine
def start(self):
print(f"Starting {self.make}...")
self.engine.start()
engine = Engine(200)
car = Car("Toyota", engine)
car.start()
# Starting Toyota...
# Engine (200hp) started!
Deep inheritance chains (3+ levels) become hard to understand and maintain. Ask yourself: "Is this an 'is-a' relationship?" If not, use composition.
Practical Example: Notification System
class Notification:
def __init__(self, recipient, message):
self.recipient = recipient
self.message = message
def send(self):
raise NotImplementedError
def __str__(self):
return f"To: {self.recipient} — {self.message}"
class EmailNotification(Notification):
def __init__(self, recipient, message, subject):
super().__init__(recipient, message)
self.subject = subject
def send(self):
print(f"Sending email to {self.recipient}")
print(f" Subject: {self.subject}")
print(f" Body: {self.message}")
class SMSNotification(Notification):
def send(self):
# SMS messages are limited to 160 chars
msg = self.message[:160]
print(f"Sending SMS to {self.recipient}: {msg}")
class PushNotification(Notification):
def __init__(self, recipient, message, app_name):
super().__init__(recipient, message)
self.app_name = app_name
def send(self):
print(f"Push [{self.app_name}] to {self.recipient}: {self.message}")
# Polymorphism — send all notifications the same way
notifications = [
EmailNotification("alice@example.com", "Your order shipped!", "Order Update"),
SMSNotification("+1234567890", "Your code is 482910"),
PushNotification("alice_device", "New message received", "ChatApp")
]
for notification in notifications:
notification.send()
print()
Summary
- Inheritance creates child classes that inherit from parent classes:
class Child(Parent) - Override methods by redefining them in the child class
super()calls the parent's version of a method- Polymorphism lets different classes respond to the same method call differently
- Use
isinstance()andissubclass()to check relationships - Prefer composition ("has-a") over inheritance ("is-a") when the relationship isn't clear
You now understand the core pillars of OOP in Python. Next up: working with JSON data — a critical skill for APIs, configs, and data exchange.