Skip to main content

👨‍👩‍👧‍👦 What is Inheritance?

Imagine a family where everyone has the same last name and some common traits:
  • Parents have certain abilities (cooking, driving, etc.)
  • Children automatically have those abilities too!
  • But children can also have their own special abilities
Inheritance in OOP works the same way:
  • A Parent Class (superclass) has some attributes and methods
  • A Child Class (subclass) automatically gets all of those
  • The child can add its own special stuff too!
Simple Definition: Inheritance = Child classes get everything from parent classes + can add their own stuff

🐾 The Classic Animal Example

Let’s start with the most common example - Animals!
# 👆 PARENT CLASS (Superclass)
class Animal:
    def __init__(self, name, age):
        self.name = name
        self.age = age
    
    def eat(self):
        print(f"{self.name} is eating... 🍽️")
    
    def sleep(self):
        print(f"{self.name} is sleeping... 💤")
    
    def make_sound(self):
        print(f"{self.name} makes a sound")

# 👇 CHILD CLASSES (Subclasses) - inherit from Animal
class Dog(Animal):  # Dog IS-A Animal
    def __init__(self, name, age, breed):
        super().__init__(name, age)  # Call parent's __init__
        self.breed = breed  # Dog's own special attribute
    
    def make_sound(self):  # Override parent's method
        print(f"{self.name} says: Woof! Woof! 🐕")
    
    def fetch(self):  # Dog's own special method
        print(f"{self.name} is fetching the ball! 🎾")

class Cat(Animal):  # Cat IS-A Animal
    def __init__(self, name, age, color):
        super().__init__(name, age)
        self.color = color
    
    def make_sound(self):
        print(f"{self.name} says: Meow! 🐱")
    
    def climb(self):  # Cat's special method
        print(f"{self.name} climbed the tree! 🌳")

# Let's test it!
dog = Dog("Buddy", 3, "Golden Retriever")
cat = Cat("Whiskers", 2, "Orange")

# Both have parent's methods
dog.eat()         # Buddy is eating... 🍽️
cat.eat()         # Whiskers is eating... 🍽️
dog.sleep()       # Buddy is sleeping... 💤

# Each has their own sound
dog.make_sound()  # Buddy says: Woof! Woof! 🐕
cat.make_sound()  # Whiskers says: Meow! 🐱

# Each has special abilities
dog.fetch()       # Buddy is fetching the ball! 🎾
cat.climb()       # Whiskers climbed the tree! 🌳

🔑 Key Concepts

1️⃣ The super() Function

super() is like calling your parent to help you:
class Parent:
    def __init__(self, name):
        self.name = name
        print(f"Parent says: Hello, {name}!")

class Child(Parent):
    def __init__(self, name, toy):
        super().__init__(name)  # 📞 Call parent's __init__ first!
        self.toy = toy          # Then add child's own stuff
        print(f"Child says: I have a {toy}!")

kid = Child("Tommy", "robot")
# Output:
# Parent says: Hello, Tommy!
# Child says: I have a robot!

2️⃣ Method Overriding

Children can replace parent methods with their own version:
class Bird(Animal):
    def make_sound(self):
        print(f"{self.name} says: Tweet tweet! 🐦")
    
    # Override eat to be more specific
    def eat(self):
        print(f"{self.name} is pecking at seeds... 🌻")

sparrow = Bird("Sparky", 1)
sparrow.eat()  # Sparky is pecking at seeds... 🌻 (Bird's version)

3️⃣ The IS-A Relationship

Inheritance creates an “IS-A” relationship:
  • Dog IS-A Animal ✅
  • Cat IS-A Animal ✅
  • Car IS-A Animal ❌ (doesn’t make sense!)
dog = Dog("Rex", 5, "German Shepherd")
print(isinstance(dog, Dog))     # True
print(isinstance(dog, Animal))  # True - Dog IS-A Animal!
print(isinstance(dog, Cat))     # False - Dog is NOT a Cat

🎮 Fun Example: RPG Character Classes

Let’s build a game with different character types:
class Character:
    """Base class for all game characters"""
    
    def __init__(self, name, health=100, mana=50):
        self.name = name
        self.health = health
        self.max_health = health
        self.mana = mana
        self.level = 1
    
    def take_damage(self, amount):
        self.health -= amount
        if self.health <= 0:
            self.health = 0
            print(f"💀 {self.name} has fallen!")
        else:
            print(f"😣 {self.name} took {amount} damage! HP: {self.health}/{self.max_health}")
    
    def heal(self, amount):
        self.health = min(self.health + amount, self.max_health)
        print(f"💚 {self.name} healed for {amount}! HP: {self.health}/{self.max_health}")
    
    def attack(self, target):
        damage = 10  # Base damage
        print(f"⚔️ {self.name} attacks {target.name}!")
        target.take_damage(damage)

# 🧙‍♂️ MAGE - Magic specialist
class Mage(Character):
    def __init__(self, name):
        super().__init__(name, health=80, mana=150)  # Less health, more mana
        self.spells = ["Fireball", "Ice Blast", "Lightning"]
    
    def cast_spell(self, spell_name, target):
        if spell_name not in self.spells:
            print(f"❌ {self.name} doesn't know {spell_name}!")
            return
        
        if self.mana < 20:
            print(f"❌ Not enough mana!")
            return
        
        self.mana -= 20
        damage = 30  # Magic is powerful!
        print(f"✨ {self.name} casts {spell_name}!")
        target.take_damage(damage)

# 🛡️ WARRIOR - Tank specialist
class Warrior(Character):
    def __init__(self, name):
        super().__init__(name, health=150, mana=20)  # More health, less mana
        self.armor = 10
        self.is_defending = False
    
    def take_damage(self, amount):  # Override to use armor
        if self.is_defending:
            amount = amount // 2
            print(f"🛡️ {self.name} blocks half the damage!")
            self.is_defending = False
        
        actual_damage = max(0, amount - self.armor)
        super().take_damage(actual_damage)  # Call parent's take_damage
    
    def defend(self):
        self.is_defending = True
        print(f"🛡️ {self.name} raises their shield!")
    
    def heavy_strike(self, target):
        print(f"💪 {self.name} uses HEAVY STRIKE!")
        target.take_damage(25)

# 🏹 ARCHER - Range specialist  
class Archer(Character):
    def __init__(self, name):
        super().__init__(name, health=90, mana=60)
        self.arrows = 20
    
    def shoot_arrow(self, target):
        if self.arrows <= 0:
            print(f"❌ {self.name} is out of arrows!")
            return
        
        self.arrows -= 1
        print(f"🏹 {self.name} shoots an arrow! (Arrows left: {self.arrows})")
        target.take_damage(18)
    
    def multi_shot(self, targets):
        """Shoot 3 arrows at multiple targets!"""
        if self.arrows < 3:
            print(f"❌ Need 3 arrows for multi-shot!")
            return
        
        self.arrows -= 3
        print(f"🏹🏹🏹 {self.name} uses MULTI-SHOT!")
        for target in targets[:3]:
            target.take_damage(12)

# 🎮 Let's play!
mage = Mage("Gandalf")
warrior = Warrior("Conan")
archer = Archer("Legolas")

print("=== Battle Begins! ===\n")

# Mage attacks warrior
mage.cast_spell("Fireball", warrior)

# Warrior defends then attacks
warrior.defend()
mage.attack(warrior)  # Will be blocked

# Warrior counter attacks
warrior.heavy_strike(mage)

# Archer joins the fight
archer.shoot_arrow(mage)

print(f"\n=== Final Status ===")
print(f"🧙‍♂️ {mage.name}: {mage.health} HP, {mage.mana} Mana")
print(f"🛡️ {warrior.name}: {warrior.health} HP")
print(f"🏹 {archer.name}: {archer.health} HP, {archer.arrows} Arrows")

📚 Types of Inheritance

Single Inheritance

One parent, one child
Animal

  Dog

Multi-level Inheritance

Chain of inheritance
Animal

Mammal

  Dog

Multiple Inheritance

Multiple parents
Flyable  Swimmable
    ↘    ↙
     Duck

Hierarchical Inheritance

One parent, many children
    Animal
   /   |   \
Dog   Cat   Bird

Multi-level Example

class Animal:
    def breathe(self):
        print("Breathing... 💨")

class Mammal(Animal):  # Inherits from Animal
    def feed_milk(self):
        print("Feeding milk to babies 🍼")

class Dog(Mammal):  # Inherits from Mammal (and thus Animal)
    def bark(self):
        print("Woof! 🐕")

dog = Dog()
dog.breathe()    # From Animal ✅
dog.feed_milk()  # From Mammal ✅
dog.bark()       # From Dog ✅

Multiple Inheritance Example

class Flyable:
    def fly(self):
        print("Flying through the sky! 🦅")

class Swimmable:
    def swim(self):
        print("Swimming in water! 🏊")

class Duck(Flyable, Swimmable):  # Inherits from BOTH
    def quack(self):
        print("Quack quack! 🦆")

duck = Duck()
duck.fly()    # From Flyable ✅
duck.swim()   # From Swimmable ✅
duck.quack()  # From Duck ✅

print("A duck can do everything!")

⚠️ When NOT to Use Inheritance

Don’t use inheritance just to share code! Use it when there’s a real IS-A relationship.

❌ Wrong: Stack inherits from List

# BAD: A Stack IS-NOT-A List (even if they share some behavior)
class Stack(list):  # Don't do this!
    def push(self, item):
        self.append(item)
    
    def pop(self):
        return super().pop()

# Problem: Stack now has all list methods!
stack = Stack()
stack.push(1)
stack.push(2)
stack.insert(0, 100)  # 😱 Users can break the stack behavior!

✅ Right: Use Composition Instead

# GOOD: Stack HAS-A list inside (composition)
class Stack:
    def __init__(self):
        self._items = []  # Stack HAS a list
    
    def push(self, item):
        self._items.append(item)
    
    def pop(self):
        return self._items.pop()
    
    def peek(self):
        return self._items[-1] if self._items else None
    
    def is_empty(self):
        return len(self._items) == 0

# Now users can only use stack operations!
stack = Stack()
stack.push(1)
stack.push(2)
# stack.insert(0, 100)  # ❌ Error! No insert method available

🧪 Practice Exercise

Create a vehicle inheritance system:
  1. Vehicle (parent): has brand, model, and a start() method
  2. Car (child): adds number of doors, and honk() method
  3. Motorcycle (child): adds engine_cc, and wheelie() method
  4. ElectricCar (child of Car): adds battery_capacity, and charge() method
class Vehicle:
    def __init__(self, brand, model):
        # TODO: Initialize brand and model
        pass
    
    def start(self):
        # TODO: Print starting message
        pass

class Car(Vehicle):
    # TODO: Add doors, override start to say "Car starting..."
    pass

class Motorcycle(Vehicle):
    # TODO: Add engine_cc, add wheelie() method
    pass

class ElectricCar(Car):
    # TODO: Add battery_capacity, add charge() method
    pass

# Test
tesla = ElectricCar("Tesla", "Model 3", 4, 75)
tesla.start()    # Should work (from Car/Vehicle)
tesla.honk()     # Should work (from Car)
tesla.charge()   # Should work (from ElectricCar)

📝 Quick Summary

ConceptDescriptionExample
Parent ClassThe class being inherited fromAnimal
Child ClassThe class that inheritsDog(Animal)
super()Calls parent’s methodssuper().__init__()
OverrideReplace parent’s methodRedefine make_sound() in child
IS-AThe relationship testDog IS-A Animal ✅

🏃 Next Up: Polymorphism

Now that you can create family trees of classes, let’s learn how the same method can behave differently for different objects!

Continue to Polymorphism →

Learn how one interface can work with many different types - like a universal remote!