Skip to main content

Documentation Index

Fetch the complete documentation index at: https://resources.devweekends.com/llms.txt

Use this file to discover all available pages before exploring further.

👨‍👩‍👧‍👦 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
Another analogy: Think of a franchise restaurant like Subway. The corporate headquarters (parent class) defines the standard menu, processes, and branding. Each franchise location (child class) inherits all of that, but can add local specials or adapt to regional tastes. The franchise gets the proven foundation for free and customizes only what needs to be different.

🐾 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.
Design reasoning: This is one of the most important judgment calls in OOP. Inheritance creates a permanent, structural relationship — the child is forever coupled to the parent. If the parent changes, every child changes too. The “IS-A” test is your guardrail: use inheritance only when the child truly is a kind of the parent in every meaningful context. When you just want to reuse some behavior, prefer composition (“HAS-A”) instead. A senior engineer would say: “Inheritance is a power tool — it can build things fast, but it can also cut you if used carelessly. Default to composition and reach for inheritance only when the IS-A relationship is genuinely true.”

❌ 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)
# PRINCIPLE: Composition over inheritance -- the stack *uses* a list
# internally but does not *expose* list behavior to callers.
class Stack:
    def __init__(self):
        self._items = []  # Stack HAS a list (not IS-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
# This is the benefit: the public interface is exactly what a stack should be.

🧪 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 ✅

Interview Insight

Common interview trap: Candidates overuse inheritance in their LLD designs. When designing a system, interviewers watch for whether you default to deep class hierarchies or use composition wisely. For example, if asked to design a notification system, a weak answer creates EmailNotification extends Notification extends Message extends BaseEntity — four levels deep. A strong answer uses composition: a Notification class has a Channel (email, SMS, push) and a Template. The moment you say “I’ll prefer composition here because these components change independently,” you signal senior-level thinking. Remember: favor shallow hierarchies (1-2 levels) and reach for composition when the relationship is “uses” rather than “is a kind of.”

Interview Deep-Dive

Strong Answer:
  • Inheritance is correct when three conditions are met: the relationship is genuinely “is-a” and will remain stable, the base class is unlikely to change its contract, and you need polymorphic substitution. The Animal/Dog/Cat hierarchy is valid — a Dog genuinely is an Animal, and the Animal contract is stable.
  • Inheritance is wrong when any of these fail. Stack-extends-List exposes methods (insert, sort) that violate stack semantics. Composition (Stack has-a list internally) gives full control over the exposed interface.
  • The deeper issue is coupling. Inheritance is the tightest coupling in OOP — the child depends on the parent’s implementation, not just its interface. Composition couples you only to the interface.
Follow-up: In the RPG example, Warrior overrides take_damage(). What if a new class is inserted between Character and Warrior in the hierarchy?This is the fragile base class problem. The super() call in Warrior dispatches according to the MRO, which changes when a new class is inserted. The new class’s take_damage() may double-apply armor or conflict with the defend logic. The fix: keep hierarchies shallow (two levels max) or use composition with a DamageHandler strategy.
Strong Answer:
  • The diamond problem occurs when Duck inherits from FlyingBird and SwimmingBird, which both inherit from Bird. Python solves this with C3 linearization — a deterministic ordering where each class is visited exactly once. You can inspect it with Duck.mro.
  • For LLD, the practical implication is: use multiple inheritance sparingly and only for orthogonal capabilities (Flyable and Swimmable). Use ABCs as “interfaces” (no shared state) for safe multiple inheritance.
  • When in doubt, prefer composition. A Warrior’s damage reduction might change at runtime (different armor), so composition with a DamageStrategy is better than an ArmorMixin.
Follow-up: When would you use a mixin versus composition?Mixins work when the behavior is orthogonal, stateless, and will not vary at runtime (LoggingMixin, SerializableMixin). If the behavior carries state or might change dynamically, use composition. The test: “Would I ever want to swap this behavior at runtime?” If yes, composition. If no, mixin is acceptable.
Strong Answer:
  • Three levels is at the boundary. It works here because each level adds genuine specialization and the hierarchy is stable. But I would question whether the third level is necessary. If the only difference is the power source, composition might be cleaner: Car has-a Powertrain interface (Gas, Electric, Hybrid). Adding hydrogen fuel cell does not require a new hierarchy branch.
  • My rule: two levels is always fine, three requires justification, four or more is almost certainly wrong. In an interview, I present the inheritance version first (faster to draw) then offer the composition alternative. This shows I know both and can judge which fits.
Follow-up: What is the LSP concern with ElectricCar overriding Car.start()?If consumer code checks the output of start() and expects “engine starting,” ElectricCar’s “silently starting” breaks the contract. The fix: define start() at the Vehicle level as “prepare for operation” without engine-specific language. Subtypes implement their own version, and consumers rely on the abstract contract.

🏃 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!