Skip to main content

🧬 Lesson 6.3: Inheritance and Polymorphism

What if you're building a game with warriors, mages, and archers? They all have names and health, but each fights differently. Do you copy-paste the shared code into three separate classes? No. You use inheritance to share the common parts and polymorphism to let each type behave in its own way.

🎯 Learning Objectives

By the end of this lesson, you will be able to:

  • Explain why inheritance exists and when to use it
  • Create a child class that extends a parent class
  • Call the parent constructor with super()
  • Override methods to customize behavior in child classes
  • Use polymorphism to treat different types through a shared interface
  • Check types with isinstance / instanceof
  • Know when to prefer composition over inheritance

Estimated Time: 60 minutes

Project: Build an RPG character system with a shared base class and specialized subclasses

📑 In This Lesson

Why Inheritance?

Imagine building classes for different animals. Dogs, cats, and birds all have names and can make sounds — but each makes a different sound. Without inheritance, you'd duplicate shared code across every class:

# ❌ Without inheritance — lots of duplicated code
class Dog:
    def __init__(self, name, age):
        self.name = name       # Duplicated!
        self.age = age         # Duplicated!

    def speak(self):
        return f"{self.name} says Woof!"

class Cat:
    def __init__(self, name, age):
        self.name = name       # Same code again!
        self.age = age         # Same code again!

    def speak(self):
        return f"{self.name} says Meow!"

class Bird:
    def __init__(self, name, age):
        self.name = name       # And again!
        self.age = age         # And again!

    def speak(self):
        return f"{self.name} says Tweet!"

# The __init__ method is identical in all three classes.
# If we add a "vaccinated" attribute, we'd change it in THREE places.
// ❌ Without inheritance — lots of duplicated code
class Dog {
    constructor(name, age) {
        this.name = name;   // Duplicated!
        this.age = age;     // Duplicated!
    }
    speak() { return `${this.name} says Woof!`; }
}

class Cat {
    constructor(name, age) {
        this.name = name;   // Same code again!
        this.age = age;     // Same code again!
    }
    speak() { return `${this.name} says Meow!`; }
}

class Bird {
    constructor(name, age) {
        this.name = name;   // And again!
        this.age = age;     // And again!
    }
    speak() { return `${this.name} says Tweet!`; }
}

// Adding a "vaccinated" field means changing THREE constructors.
// ❌ Without inheritance — lots of duplicated code
class Dog
{
    public string Name;   // Duplicated!
    public int Age;       // Duplicated!

    public Dog(string name, int age)
    { Name = name; Age = age; }

    public string Speak() => $"{Name} says Woof!";
}

class Cat
{
    public string Name;   // Same fields again!
    public int Age;

    public Cat(string name, int age)
    { Name = name; Age = age; }

    public string Speak() => $"{Name} says Meow!";
}

// Adding a "Vaccinated" field means changing EVERY class.

Inheritance solves this by letting you define shared attributes and methods once in a parent class (also called a base class or superclass), and then create child classes (also called subclasses or derived classes) that automatically get everything the parent has — plus their own specializations.

classDiagram class Animal { +name: string +age: int +speak(): string +describe(): string } class Dog { +breed: string +speak(): string +fetch(): void } class Cat { +indoor: bool +speak(): string +purr(): void } class Bird { +can_fly: bool +speak(): string +fly(): void } Animal <|-- Dog : extends Animal <|-- Cat : extends Animal <|-- Bird : extends
🎓 Instructor Note: Delivery Guidance

Start with the "duplication problem" code and ask students: "What happens if we want to add a vaccinated attribute to every animal?" They'll see the pain of changing three classes. Then show the diagram — the parent holds the shared parts, children add their own. The class diagram is a great visual anchor. Real-world analogy: a Vehicle parent class with Car, Truck, and Motorcycle children. Students immediately grasp that all vehicles have engines and wheels, but each type has unique features.

Creating Subclasses

Here's the inheritance version. The Animal parent class holds everything shared, and each child class adds only what's unique.

# Parent class (base class / superclass)
class Animal:
    def __init__(self, name, age):
        self.name = name
        self.age = age

    def speak(self):
        return f"{self.name} makes a sound"

    def describe(self):
        return f"{self.name}, age {self.age}"

# Child class — inherits from Animal
class Dog(Animal):
    pass  # Inherits everything, adds nothing (yet)

class Cat(Animal):
    pass

# Dogs and Cats get Animal's attributes and methods for FREE
fido = Dog("Fido", 3)
whiskers = Cat("Whiskers", 5)

print(fido.describe())      # Fido, age 3
print(whiskers.describe())  # Whiskers, age 5
print(fido.speak())         # Fido makes a sound
print(whiskers.speak())     # Whiskers makes a sound

# They're different types but share behavior
print(type(fido))      # <class 'Dog'>
print(type(whiskers))  # <class 'Cat'>
// Parent class
class Animal {
    constructor(name, age) {
        this.name = name;
        this.age = age;
    }

    speak() {
        return `${this.name} makes a sound`;
    }

    describe() {
        return `${this.name}, age ${this.age}`;
    }
}

// Child class — extends Animal
class Dog extends Animal {
    // Inherits everything, adds nothing (yet)
}

class Cat extends Animal {}

// Dogs and Cats get Animal's constructor and methods for FREE
let fido = new Dog("Fido", 3);
let whiskers = new Cat("Whiskers", 5);

console.log(fido.describe());      // Fido, age 3
console.log(whiskers.describe());  // Whiskers, age 5
console.log(fido.speak());         // Fido makes a sound
console.log(whiskers.speak());     // Whiskers makes a sound

console.log(fido instanceof Dog);     // true
console.log(fido instanceof Animal);  // true
// Parent class
class Animal
{
    public string Name;
    public int Age;

    public Animal(string name, int age)
    {
        Name = name;
        Age = age;
    }

    public virtual string Speak()
    {
        return $"{Name} makes a sound";
    }

    public string Describe()
    {
        return $"{Name}, age {Age}";
    }
}

// Child class — inherits from Animal
class Dog : Animal
{
    public Dog(string name, int age) : base(name, age) { }
}

class Cat : Animal
{
    public Cat(string name, int age) : base(name, age) { }
}

Dog fido = new Dog("Fido", 3);
Cat whiskers = new Cat("Whiskers", 5);

Console.WriteLine(fido.Describe());      // Fido, age 3
Console.WriteLine(whiskers.Describe());  // Whiskers, age 5
Console.WriteLine(fido.Speak());         // Fido makes a sound

Inheritance Syntax Comparison

Feature 🐍 Python ⚡ JavaScript 🔷 C#
Extend a class class Dog(Animal): class Dog extends Animal class Dog : Animal
Keyword Parentheses () extends Colon :
Auto-inherit constructor? Yes (if child has no __init__) Yes (if child has no constructor) No — must call : base()

⚠️ C# Requires Explicit Constructor Chaining

In C#, child classes must explicitly call the parent constructor using : base(...). Python and JavaScript automatically use the parent constructor if the child doesn't define its own. This is another example of C#'s philosophy of making everything explicit.

Calling the Parent: super()

When a child class needs its own constructor — to add extra attributes — it must still call the parent's constructor to set up the inherited parts. That's what super() does.

class Animal:
    def __init__(self, name, age):
        self.name = name
        self.age = age

    def speak(self):
        return f"{self.name} makes a sound"

class Dog(Animal):
    def __init__(self, name, age, breed):
        super().__init__(name, age)  # Call Animal's __init__
        self.breed = breed           # Dog-specific attribute

    def fetch(self, item):
        return f"{self.name} fetches the {item}!"

class Cat(Animal):
    def __init__(self, name, age, indoor=True):
        super().__init__(name, age)
        self.indoor = indoor

    def purr(self):
        return f"{self.name} purrs..."

# Create with child-specific attributes
fido = Dog("Fido", 3, "Golden Retriever")
whiskers = Cat("Whiskers", 5, indoor=False)

# Inherited methods still work
print(fido.describe())    # Fido, age 3

# Child-specific methods
print(fido.fetch("ball"))     # Fido fetches the ball!
print(fido.breed)             # Golden Retriever
print(whiskers.purr())        # Whiskers purrs...
print(whiskers.indoor)        # False
class Animal {
    constructor(name, age) {
        this.name = name;
        this.age = age;
    }

    speak() { return `${this.name} makes a sound`; }
    describe() { return `${this.name}, age ${this.age}`; }
}

class Dog extends Animal {
    constructor(name, age, breed) {
        super(name, age);      // Call Animal's constructor
        this.breed = breed;    // Dog-specific property
    }

    fetch(item) {
        return `${this.name} fetches the ${item}!`;
    }
}

class Cat extends Animal {
    constructor(name, age, indoor = true) {
        super(name, age);
        this.indoor = indoor;
    }

    purr() { return `${this.name} purrs...`; }
}

let fido = new Dog("Fido", 3, "Golden Retriever");
let whiskers = new Cat("Whiskers", 5, false);

console.log(fido.describe());       // Fido, age 3
console.log(fido.fetch("ball"));    // Fido fetches the ball!
console.log(whiskers.purr());       // Whiskers purrs...
class Animal
{
    public string Name;
    public int Age;

    public Animal(string name, int age)
    {
        Name = name;
        Age = age;
    }

    public virtual string Speak() => $"{Name} makes a sound";
    public string Describe() => $"{Name}, age {Age}";
}

class Dog : Animal
{
    public string Breed;

    public Dog(string name, int age, string breed)
        : base(name, age)       // Call Animal's constructor
    {
        Breed = breed;          // Dog-specific
    }

    public string Fetch(string item) => $"{Name} fetches the {item}!";
}

class Cat : Animal
{
    public bool Indoor;

    public Cat(string name, int age, bool indoor = true)
        : base(name, age)
    {
        Indoor = indoor;
    }

    public string Purr() => $"{Name} purrs...";
}

Dog fido = new Dog("Fido", 3, "Golden Retriever");
Cat whiskers = new Cat("Whiskers", 5, false);

Console.WriteLine(fido.Describe());     // Fido, age 3
Console.WriteLine(fido.Fetch("ball"));  // Fido fetches the ball!
Console.WriteLine(whiskers.Purr());     // Whiskers purrs...

💡 super() Must Come First

In JavaScript, you must call super() before using this in a child constructor — the engine will throw an error otherwise. Python and C# have the same recommendation but are slightly more flexible about ordering. Best practice in all three: call super() as the first line of the child constructor.

super() Syntax Comparison

Feature 🐍 Python ⚡ JavaScript 🔷 C#
Call parent constructor super().__init__(args) super(args) : base(args)
Call parent method super().method() super.method() base.Method()
Where it goes Inside __init__ Inside constructor After : in signature
🎓 Instructor Note: Delivery Guidance

The C# syntax is the most different here — : base(name, age) appears in the constructor signature, not in the body. Draw attention to this. Python's super().__init__() looks odd because you're calling __init__ explicitly — remind students that Python's dunder methods are always explicit. JavaScript's super() in the constructor is the cleanest syntax. A common mistake: forgetting super() entirely, which means the parent's attributes never get set.

Overriding Methods

A child class can override a parent method by defining a method with the same name. The child's version replaces the parent's — but you can still call the parent's version with super() if needed.

class Animal:
    def __init__(self, name, age):
        self.name = name
        self.age = age

    def speak(self):
        return f"{self.name} makes a sound"

    def describe(self):
        return f"{self.name}, age {self.age}"

class Dog(Animal):
    def __init__(self, name, age, breed):
        super().__init__(name, age)
        self.breed = breed

    # Override speak — replace parent's version
    def speak(self):
        return f"{self.name} says Woof!"

    # Override describe — extend parent's version
    def describe(self):
        base = super().describe()  # Call parent's describe()
        return f"{base}, {self.breed}"

class Cat(Animal):
    def __init__(self, name, age, indoor=True):
        super().__init__(name, age)
        self.indoor = indoor

    def speak(self):
        return f"{self.name} says Meow!"

    def describe(self):
        base = super().describe()
        status = "indoor" if self.indoor else "outdoor"
        return f"{base}, {status} cat"

fido = Dog("Fido", 3, "Golden Retriever")
whiskers = Cat("Whiskers", 5)

print(fido.speak())       # Fido says Woof!      (overridden)
print(whiskers.speak())   # Whiskers says Meow!   (overridden)

print(fido.describe())    # Fido, age 3, Golden Retriever
print(whiskers.describe()) # Whiskers, age 5, indoor cat
class Animal {
    constructor(name, age) {
        this.name = name;
        this.age = age;
    }

    speak() { return `${this.name} makes a sound`; }
    describe() { return `${this.name}, age ${this.age}`; }
}

class Dog extends Animal {
    constructor(name, age, breed) {
        super(name, age);
        this.breed = breed;
    }

    // Override speak — replace parent's version
    speak() {
        return `${this.name} says Woof!`;
    }

    // Override describe — extend parent's version
    describe() {
        return `${super.describe()}, ${this.breed}`;
    }
}

class Cat extends Animal {
    constructor(name, age, indoor = true) {
        super(name, age);
        this.indoor = indoor;
    }

    speak() { return `${this.name} says Meow!`; }

    describe() {
        let status = this.indoor ? "indoor" : "outdoor";
        return `${super.describe()}, ${status} cat`;
    }
}

let fido = new Dog("Fido", 3, "Golden Retriever");
let whiskers = new Cat("Whiskers", 5);

console.log(fido.speak());       // Fido says Woof!
console.log(fido.describe());    // Fido, age 3, Golden Retriever
console.log(whiskers.describe()); // Whiskers, age 5, indoor cat
class Animal
{
    public string Name;
    public int Age;

    public Animal(string name, int age)
    { Name = name; Age = age; }

    // 'virtual' = "children can override this"
    public virtual string Speak() => $"{Name} makes a sound";
    public virtual string Describe() => $"{Name}, age {Age}";
}

class Dog : Animal
{
    public string Breed;

    public Dog(string name, int age, string breed)
        : base(name, age) { Breed = breed; }

    // 'override' = "I'm replacing the parent's version"
    public override string Speak() => $"{Name} says Woof!";

    public override string Describe() =>
        $"{base.Describe()}, {Breed}";
}

class Cat : Animal
{
    public bool Indoor;

    public Cat(string name, int age, bool indoor = true)
        : base(name, age) { Indoor = indoor; }

    public override string Speak() => $"{Name} says Meow!";

    public override string Describe()
    {
        string status = Indoor ? "indoor" : "outdoor";
        return $"{base.Describe()}, {status} cat";
    }
}

Dog fido = new Dog("Fido", 3, "Golden Retriever");
Cat whiskers = new Cat("Whiskers", 5);

Console.WriteLine(fido.Speak());       // Fido says Woof!
Console.WriteLine(fido.Describe());    // Fido, age 3, Golden Retriever
Console.WriteLine(whiskers.Describe()); // Whiskers, age 5, indoor cat

⚠️ C# Requires virtual and override

C# is the strictest language here. The parent method must be marked virtual to allow overriding, and the child must use the override keyword. Without these, C# hides the parent method instead of replacing it — a subtle bug. Python and JavaScript let you override freely without any keywords.

sequenceDiagram participant Code as Your Code participant Dog as fido (Dog) participant Animal as Animal (Parent) Code->>Dog: fido.speak() Note right of Dog: Dog has speak()
→ Uses Dog's version Dog-->>Code: "Fido says Woof!" Code->>Dog: fido.describe() Dog->>Animal: super().describe() Animal-->>Dog: "Fido, age 3" Note right of Dog: Adds breed info Dog-->>Code: "Fido, age 3, Golden Retriever"

Polymorphism in Action

Polymorphism (Greek for "many forms") means you can treat different types through a shared interface. If Dog, Cat, and Bird all have a speak() method, you can call speak() on any of them without knowing or caring which specific type it is.

class Animal:
    def __init__(self, name):
        self.name = name

    def speak(self):
        return f"{self.name} makes a sound"

class Dog(Animal):
    def speak(self):
        return f"{self.name} says Woof!"

class Cat(Animal):
    def speak(self):
        return f"{self.name} says Meow!"

class Bird(Animal):
    def speak(self):
        return f"{self.name} says Tweet!"

# A list of DIFFERENT types — but all are Animals
animals = [Dog("Fido"), Cat("Whiskers"), Bird("Tweety"), Dog("Rex")]

# Polymorphism: same method call, different behavior
for animal in animals:
    print(animal.speak())
# Fido says Woof!
# Whiskers says Meow!
# Tweety says Tweet!
# Rex says Woof!

# Works with any function that expects an Animal
def introduce(animal):
    """Works with ANY animal — doesn't care about the specific type"""
    print(f"Meet {animal.name}! {animal.speak()}")

introduce(Dog("Buddy"))    # Meet Buddy! Buddy says Woof!
introduce(Cat("Luna"))     # Meet Luna! Luna says Meow!
introduce(Bird("Polly"))   # Meet Polly! Polly says Tweet!
class Animal {
    constructor(name) { this.name = name; }
    speak() { return `${this.name} makes a sound`; }
}

class Dog extends Animal {
    speak() { return `${this.name} says Woof!`; }
}

class Cat extends Animal {
    speak() { return `${this.name} says Meow!`; }
}

class Bird extends Animal {
    speak() { return `${this.name} says Tweet!`; }
}

// A list of DIFFERENT types — all are Animals
let animals = [new Dog("Fido"), new Cat("Whiskers"),
               new Bird("Tweety"), new Dog("Rex")];

// Polymorphism: same method call, different behavior
animals.forEach(animal => console.log(animal.speak()));
// Fido says Woof!
// Whiskers says Meow!
// Tweety says Tweet!
// Rex says Woof!

// Works with any function that expects an Animal
function introduce(animal) {
    console.log(`Meet ${animal.name}! ${animal.speak()}`);
}

introduce(new Dog("Buddy"));   // Meet Buddy! Buddy says Woof!
introduce(new Cat("Luna"));    // Meet Luna! Luna says Meow!
class Animal
{
    public string Name;
    public Animal(string name) { Name = name; }
    public virtual string Speak() => $"{Name} makes a sound";
}

class Dog : Animal
{
    public Dog(string name) : base(name) { }
    public override string Speak() => $"{Name} says Woof!";
}

class Cat : Animal
{
    public Cat(string name) : base(name) { }
    public override string Speak() => $"{Name} says Meow!";
}

class Bird : Animal
{
    public Bird(string name) : base(name) { }
    public override string Speak() => $"{Name} says Tweet!";
}

// A list of the PARENT type — holds any child
List<Animal> animals = new List<Animal>
{
    new Dog("Fido"), new Cat("Whiskers"),
    new Bird("Tweety"), new Dog("Rex")
};

// Polymorphism: same method call, different behavior
foreach (Animal animal in animals)
    Console.WriteLine(animal.Speak());

// Works with any method that accepts Animal
static void Introduce(Animal animal)
{
    Console.WriteLine($"Meet {animal.Name}! {animal.Speak()}");
}

Introduce(new Dog("Buddy"));
Introduce(new Cat("Luna"));

✅ Why Polymorphism Matters

Polymorphism lets you write code that works with any type in a family — without if/else chains checking which type you have. The introduce() function doesn't contain if dog... else if cat.... It just calls speak() and the right version runs automatically. This makes code extensible: you can add a new Snake class and introduce() works immediately without changes.

🎓 Instructor Note: Delivery Guidance

Polymorphism is the "aha!" moment of OOP. Walk through the animals loop step by step: "When the loop reaches Fido, Python looks at Fido's actual type (Dog), finds Dog's speak(), and calls it. When it reaches Whiskers, it finds Cat's speak()." The key insight: the same code (animal.speak()) does different things depending on the object's type. C# students should note that List<Animal> can hold any child type — this is polymorphism through the type system.

Type Checking

Sometimes you do need to know what specific type an object is — especially when a child class has methods the parent doesn't.

fido = Dog("Fido", 3, "Golden Retriever")
whiskers = Cat("Whiskers", 5)

# isinstance() — checks if object is a type (or a subtype)
print(isinstance(fido, Dog))     # True
print(isinstance(fido, Animal))  # True  ← Dog IS an Animal
print(isinstance(fido, Cat))     # False

# type() — exact type only
print(type(fido) == Dog)         # True
print(type(fido) == Animal)      # False ← exact type is Dog, not Animal

# Practical use: type-specific behavior
animals = [Dog("Fido", 3, "Lab"), Cat("Whiskers", 5), Dog("Rex", 2, "Poodle")]

dogs = [a for a in animals if isinstance(a, Dog)]
print(f"Dogs: {[d.name for d in dogs]}")  # Dogs: ['Fido', 'Rex']

# Only call fetch() on dogs
for animal in animals:
    if isinstance(animal, Dog):
        print(animal.fetch("ball"))
let fido = new Dog("Fido", 3, "Golden Retriever");
let whiskers = new Cat("Whiskers", 5);

// instanceof — checks if object is a type (or subtype)
console.log(fido instanceof Dog);     // true
console.log(fido instanceof Animal);  // true  ← Dog IS an Animal
console.log(fido instanceof Cat);     // false

// constructor.name — get the exact class name
console.log(fido.constructor.name);   // "Dog"

// Practical use
let animals = [new Dog("Fido", 3, "Lab"), new Cat("Whiskers", 5),
               new Dog("Rex", 2, "Poodle")];

let dogs = animals.filter(a => a instanceof Dog);
console.log(dogs.map(d => d.name));  // ['Fido', 'Rex']

for (let animal of animals) {
    if (animal instanceof Dog) {
        console.log(animal.fetch("ball"));
    }
}
Dog fido = new Dog("Fido", 3, "Golden Retriever");
Cat whiskers = new Cat("Whiskers", 5);

// 'is' keyword — checks type (including subtypes)
Console.WriteLine(fido is Dog);     // True
Console.WriteLine(fido is Animal);  // True  ← Dog IS an Animal
Console.WriteLine(fido is Cat);     // False

// GetType() — exact type
Console.WriteLine(fido.GetType().Name);  // "Dog"

// 'is' with pattern matching (C# 7+)
Animal mystery = new Dog("Rex", 2, "Poodle");
if (mystery is Dog dog)
{
    // 'dog' is now a Dog variable — no cast needed!
    Console.WriteLine(dog.Fetch("ball"));
}

// Filter by type with LINQ
var animals = new List<Animal>
{
    new Dog("Fido", 3, "Lab"), new Cat("Whiskers", 5),
    new Dog("Rex", 2, "Poodle")
};

var dogs = animals.OfType<Dog>().ToList();
Console.WriteLine(string.Join(", ", dogs.Select(d => d.Name)));
// Fido, Rex
Check 🐍 Python ⚡ JavaScript 🔷 C#
Is this type or subtype? isinstance(obj, Type) obj instanceof Type obj is Type
Exact type name type(obj).__name__ obj.constructor.name obj.GetType().Name
Filter by type isinstance in comprehension .filter() with instanceof .OfType<T>()

Inheritance vs. Composition

Inheritance is powerful, but it's not always the right choice. There's an alternative called composition — instead of "is-a" relationships, you build "has-a" relationships.

Approach Relationship Example When to Use
Inheritance "is-a" A Dog is an Animal True subtype relationship, shared identity
Composition "has-a" A Car has an Engine Objects contain other objects, flexible assembly
# Composition: A character HAS-A weapon and HAS-A armor
class Weapon:
    def __init__(self, name, damage):
        self.name = name
        self.damage = damage

    def __str__(self):
        return f"{self.name} ({self.damage} dmg)"

class Armor:
    def __init__(self, name, defense):
        self.name = name
        self.defense = defense

    def __str__(self):
        return f"{self.name} ({self.defense} def)"

class Character:
    def __init__(self, name, health=100):
        self.name = name
        self.health = health
        self.weapon = None     # HAS-A weapon
        self.armor = None      # HAS-A armor

    def equip_weapon(self, weapon):
        self.weapon = weapon
        print(f"{self.name} equips {weapon}")

    def equip_armor(self, armor):
        self.armor = armor
        print(f"{self.name} equips {armor}")

    def attack(self, target):
        damage = self.weapon.damage if self.weapon else 1
        defense = target.armor.defense if target.armor else 0
        actual = max(damage - defense, 0)
        target.health -= actual
        print(f"{self.name} attacks {target.name} for {actual} damage!")

# Mix and match — no rigid class hierarchy needed
hero = Character("Alice")
hero.equip_weapon(Weapon("Flame Sword", 25))
hero.equip_armor(Armor("Dragon Shield", 10))

enemy = Character("Goblin", 50)
enemy.equip_weapon(Weapon("Rusty Dagger", 8))

hero.attack(enemy)  # Alice attacks Goblin for 25 damage!
enemy.attack(hero)  # Goblin attacks Alice for 0 damage! (8 - 10 def)
class Weapon {
    constructor(name, damage) {
        this.name = name;
        this.damage = damage;
    }
    toString() { return `${this.name} (${this.damage} dmg)`; }
}

class Armor {
    constructor(name, defense) {
        this.name = name;
        this.defense = defense;
    }
    toString() { return `${this.name} (${this.defense} def)`; }
}

class Character {
    constructor(name, health = 100) {
        this.name = name;
        this.health = health;
        this.weapon = null;
        this.armor = null;
    }

    equipWeapon(weapon) {
        this.weapon = weapon;
        console.log(`${this.name} equips ${weapon}`);
    }

    equipArmor(armor) {
        this.armor = armor;
        console.log(`${this.name} equips ${armor}`);
    }

    attack(target) {
        let damage = this.weapon ? this.weapon.damage : 1;
        let defense = target.armor ? target.armor.defense : 0;
        let actual = Math.max(damage - defense, 0);
        target.health -= actual;
        console.log(`${this.name} attacks ${target.name} for ${actual} damage!`);
    }
}

let hero = new Character("Alice");
hero.equipWeapon(new Weapon("Flame Sword", 25));
hero.equipArmor(new Armor("Dragon Shield", 10));

let enemy = new Character("Goblin", 50);
enemy.equipWeapon(new Weapon("Rusty Dagger", 8));

hero.attack(enemy);  // Alice attacks Goblin for 25 damage!
enemy.attack(hero);  // Goblin attacks Alice for 0 damage!
class Weapon
{
    public string Name;
    public int Damage;
    public Weapon(string name, int damage) { Name = name; Damage = damage; }
    public override string ToString() => $"{Name} ({Damage} dmg)";
}

class Armor
{
    public string Name;
    public int Defense;
    public Armor(string name, int defense) { Name = name; Defense = defense; }
    public override string ToString() => $"{Name} ({Defense} def)";
}

class Character
{
    public string Name;
    public int Health;
    public Weapon? Weapon;
    public Armor? Armor;

    public Character(string name, int health = 100)
    { Name = name; Health = health; }

    public void EquipWeapon(Weapon weapon)
    { Weapon = weapon; Console.WriteLine($"{Name} equips {weapon}"); }

    public void EquipArmor(Armor armor)
    { Armor = armor; Console.WriteLine($"{Name} equips {armor}"); }

    public void Attack(Character target)
    {
        int damage = Weapon?.Damage ?? 1;
        int defense = target.Armor?.Defense ?? 0;
        int actual = Math.Max(damage - defense, 0);
        target.Health -= actual;
        Console.WriteLine($"{Name} attacks {target.Name} for {actual} damage!");
    }
}

var hero = new Character("Alice");
hero.EquipWeapon(new Weapon("Flame Sword", 25));
hero.EquipArmor(new Armor("Dragon Shield", 10));

var enemy = new Character("Goblin", 50);
enemy.EquipWeapon(new Weapon("Rusty Dagger", 8));

hero.Attack(enemy);
enemy.Attack(hero);

💡 The Guideline

Use inheritance when the relationship is truly "is-a" — a Dog truly is an Animal. Use composition when the relationship is "has-a" — a Character has a Weapon. Composition is more flexible because you can swap components at runtime (change weapons mid-game). A common rule of thumb in software design: "Favor composition over inheritance."

🎓 Instructor Note: Delivery Guidance

The RPG equipment example makes composition intuitive — students immediately understand that a character can swap weapons without changing their class. Contrast this with an inheritance approach: would you have SwordCharacter, BowCharacter, MageCharacter? What if a character can use multiple weapon types? Inheritance breaks down, composition thrives. This sets up good design thinking. Don't demonize inheritance — it's the right tool when the "is-a" relationship is genuine.

Exercises

🏋️ Exercise 1: Shape Hierarchy

Objective: Create a shape system with inheritance:

  • Shape base class with a name attribute and area() / perimeter() methods (return 0)
  • Rectangle(width, height) — overrides area() and perimeter()
  • Circle(radius) — overrides area() and perimeter()
  • Square(side) — inherits from Rectangle (a square is a rectangle with equal sides)
  • Each class should have a string representation showing its dimensions
  • Create a list of mixed shapes and print the total area of all shapes combined
✅ Solution
import math

class Shape:
    def __init__(self, name):
        self.name = name

    def area(self):
        return 0

    def perimeter(self):
        return 0

    def __str__(self):
        return f"{self.name}: area={self.area():.2f}, perimeter={self.perimeter():.2f}"

class Rectangle(Shape):
    def __init__(self, width, height):
        super().__init__("Rectangle")
        self.width = width
        self.height = height

    def area(self):
        return self.width * self.height

    def perimeter(self):
        return 2 * (self.width + self.height)

    def __str__(self):
        return f"Rectangle({self.width}x{self.height}): area={self.area():.2f}"

class Circle(Shape):
    def __init__(self, radius):
        super().__init__("Circle")
        self.radius = radius

    def area(self):
        return math.pi * self.radius ** 2

    def perimeter(self):
        return 2 * math.pi * self.radius

    def __str__(self):
        return f"Circle(r={self.radius}): area={self.area():.2f}"

class Square(Rectangle):
    def __init__(self, side):
        super().__init__(side, side)  # Rectangle with equal sides
        self.name = "Square"

    def __str__(self):
        return f"Square({self.width}): area={self.area():.2f}"

# Polymorphism in action
shapes = [Rectangle(5, 3), Circle(4), Square(6), Rectangle(10, 2), Circle(1)]

for shape in shapes:
    print(shape)

total = sum(s.area() for s in shapes)
print(f"\nTotal area: {total:.2f}")
class Shape {
    constructor(name) { this.name = name; }
    area() { return 0; }
    perimeter() { return 0; }
    toString() {
        return `${this.name}: area=${this.area().toFixed(2)}`;
    }
}

class Rectangle extends Shape {
    constructor(width, height) {
        super("Rectangle");
        this.width = width;
        this.height = height;
    }
    area() { return this.width * this.height; }
    perimeter() { return 2 * (this.width + this.height); }
    toString() { return `Rectangle(${this.width}x${this.height}): area=${this.area().toFixed(2)}`; }
}

class Circle extends Shape {
    constructor(radius) {
        super("Circle");
        this.radius = radius;
    }
    area() { return Math.PI * this.radius ** 2; }
    perimeter() { return 2 * Math.PI * this.radius; }
    toString() { return `Circle(r=${this.radius}): area=${this.area().toFixed(2)}`; }
}

class Square extends Rectangle {
    constructor(side) {
        super(side, side);
        this.name = "Square";
    }
    toString() { return `Square(${this.width}): area=${this.area().toFixed(2)}`; }
}

let shapes = [new Rectangle(5, 3), new Circle(4), new Square(6),
              new Rectangle(10, 2), new Circle(1)];

shapes.forEach(s => console.log(`${s}`));
let total = shapes.reduce((sum, s) => sum + s.area(), 0);
console.log(`\nTotal area: ${total.toFixed(2)}`);
class Shape
{
    public string Name;
    public Shape(string name) { Name = name; }
    public virtual double Area() => 0;
    public virtual double Perimeter() => 0;
    public override string ToString() =>
        $"{Name}: area={Area():F2}";
}

class Rectangle : Shape
{
    public double Width, Height;
    public Rectangle(double w, double h) : base("Rectangle")
    { Width = w; Height = h; }
    public override double Area() => Width * Height;
    public override double Perimeter() => 2 * (Width + Height);
    public override string ToString() =>
        $"Rectangle({Width}x{Height}): area={Area():F2}";
}

class Circle : Shape
{
    public double Radius;
    public Circle(double r) : base("Circle") { Radius = r; }
    public override double Area() => Math.PI * Radius * Radius;
    public override double Perimeter() => 2 * Math.PI * Radius;
    public override string ToString() =>
        $"Circle(r={Radius}): area={Area():F2}";
}

class Square : Rectangle
{
    public Square(double side) : base(side, side) { Name = "Square"; }
    public override string ToString() =>
        $"Square({Width}): area={Area():F2}";
}

var shapes = new List<Shape>
{
    new Rectangle(5, 3), new Circle(4), new Square(6),
    new Rectangle(10, 2), new Circle(1)
};

shapes.ForEach(s => Console.WriteLine(s));
double total = shapes.Sum(s => s.Area());
Console.WriteLine($"\nTotal area: {total:F2}");

🏋️ Exercise 2: RPG Character System

Objective: Build an RPG with inheritance and composition:

  • Character base class: name, health, attack_power, attack(target), is_alive()
  • Warrior: extra armor attribute, takes reduced damage
  • Mage: extra mana attribute, cast_spell(target) that costs mana but does double damage
  • Archer: extra arrows attribute, shoot(target) that costs an arrow
  • Create a party vs. party battle simulation
✅ Solution
import random

class Character:
    def __init__(self, name, health, attack_power):
        self.name = name
        self.health = health
        self.attack_power = attack_power

    def is_alive(self):
        return self.health > 0

    def take_damage(self, amount):
        self.health -= amount
        if self.health < 0:
            self.health = 0

    def attack(self, target):
        damage = random.randint(self.attack_power // 2, self.attack_power)
        target.take_damage(damage)
        print(f"  {self.name} attacks {target.name} for {damage} damage!")

    def __str__(self):
        status = f"HP:{self.health}" if self.is_alive() else "DEFEATED"
        return f"{self.name} ({self.__class__.__name__}) [{status}]"

class Warrior(Character):
    def __init__(self, name, health=120, attack_power=20):
        super().__init__(name, health, attack_power)
        self.armor = 5

    def take_damage(self, amount):
        reduced = max(amount - self.armor, 0)
        super().take_damage(reduced)

class Mage(Character):
    def __init__(self, name, health=80, attack_power=15):
        super().__init__(name, health, attack_power)
        self.mana = 50

    def cast_spell(self, target):
        if self.mana >= 10:
            self.mana -= 10
            damage = self.attack_power * 2
            target.take_damage(damage)
            print(f"  🔥 {self.name} casts a spell on {target.name} for {damage} damage! (Mana: {self.mana})")
        else:
            print(f"  {self.name} is out of mana! Using basic attack.")
            self.attack(target)

class Archer(Character):
    def __init__(self, name, health=90, attack_power=18):
        super().__init__(name, health, attack_power)
        self.arrows = 10

    def shoot(self, target):
        if self.arrows > 0:
            self.arrows -= 1
            damage = self.attack_power + random.randint(5, 15)
            target.take_damage(damage)
            print(f"  🏹 {self.name} shoots {target.name} for {damage} damage! (Arrows: {self.arrows})")
        else:
            print(f"  {self.name} is out of arrows! Using basic attack.")
            self.attack(target)

# Battle simulation
heroes = [Warrior("Alice"), Mage("Merlin"), Archer("Robin")]
enemies = [Warrior("Orc Chief"), Mage("Dark Wizard"), Archer("Goblin Scout", 60, 12)]

def battle_round(team_a, team_b, round_num):
    print(f"\n=== Round {round_num} ===")
    for attacker in team_a + team_b:
        if not attacker.is_alive():
            continue
        # Pick a living target from the other team
        if attacker in team_a:
            targets = [t for t in team_b if t.is_alive()]
        else:
            targets = [t for t in team_a if t.is_alive()]
        if not targets:
            break
        target = random.choice(targets)

        # Use special ability or basic attack
        if isinstance(attacker, Mage):
            attacker.cast_spell(target)
        elif isinstance(attacker, Archer):
            attacker.shoot(target)
        else:
            attacker.attack(target)

# Run the battle
for round_num in range(1, 11):
    battle_round(heroes, enemies, round_num)
    alive_heroes = [h for h in heroes if h.is_alive()]
    alive_enemies = [e for e in enemies if e.is_alive()]
    if not alive_heroes or not alive_enemies:
        break

print("\n=== Final Status ===")
for char in heroes + enemies:
    print(f"  {char}")
class Character {
    constructor(name, health, attackPower) {
        this.name = name;
        this.health = health;
        this.attackPower = attackPower;
    }

    isAlive() { return this.health > 0; }

    takeDamage(amount) {
        this.health -= amount;
        if (this.health < 0) this.health = 0;
    }

    attack(target) {
        let damage = Math.floor(Math.random() * (this.attackPower / 2)) + Math.ceil(this.attackPower / 2);
        target.takeDamage(damage);
        console.log(`  ${this.name} attacks ${target.name} for ${damage} damage!`);
    }

    toString() {
        let status = this.isAlive() ? `HP:${this.health}` : "DEFEATED";
        return `${this.name} (${this.constructor.name}) [${status}]`;
    }
}

class Warrior extends Character {
    constructor(name, health = 120, attackPower = 20) {
        super(name, health, attackPower);
        this.armor = 5;
    }
    takeDamage(amount) { super.takeDamage(Math.max(amount - this.armor, 0)); }
}

class Mage extends Character {
    constructor(name, health = 80, attackPower = 15) {
        super(name, health, attackPower);
        this.mana = 50;
    }

    castSpell(target) {
        if (this.mana >= 10) {
            this.mana -= 10;
            let damage = this.attackPower * 2;
            target.takeDamage(damage);
            console.log(`  🔥 ${this.name} casts spell on ${target.name} for ${damage}! (Mana: ${this.mana})`);
        } else {
            this.attack(target);
        }
    }
}

class Archer extends Character {
    constructor(name, health = 90, attackPower = 18) {
        super(name, health, attackPower);
        this.arrows = 10;
    }

    shoot(target) {
        if (this.arrows > 0) {
            this.arrows--;
            let damage = this.attackPower + Math.floor(Math.random() * 11) + 5;
            target.takeDamage(damage);
            console.log(`  🏹 ${this.name} shoots ${target.name} for ${damage}! (Arrows: ${this.arrows})`);
        } else {
            this.attack(target);
        }
    }
}

let heroes = [new Warrior("Alice"), new Mage("Merlin"), new Archer("Robin")];
let enemies = [new Warrior("Orc Chief"), new Mage("Dark Wizard"), new Archer("Goblin Scout", 60, 12)];

console.log("=== Battle! ===");
for (let char of [...heroes, ...enemies]) {
    if (!char.isAlive()) continue;
    let targets = (heroes.includes(char) ? enemies : heroes).filter(t => t.isAlive());
    if (!targets.length) break;
    let target = targets[Math.floor(Math.random() * targets.length)];
    if (char instanceof Mage) char.castSpell(target);
    else if (char instanceof Archer) char.shoot(target);
    else char.attack(target);
}

console.log("\n=== Status ===");
[...heroes, ...enemies].forEach(c => console.log(`  ${c}`));
class Character
{
    public string Name;
    public int Health;
    public int AttackPower;
    private static Random rng = new();

    public Character(string name, int health, int attackPower)
    { Name = name; Health = health; AttackPower = attackPower; }

    public bool IsAlive() => Health > 0;

    public virtual void TakeDamage(int amount)
    { Health -= amount; if (Health < 0) Health = 0; }

    public virtual void Attack(Character target)
    {
        int damage = rng.Next(AttackPower / 2, AttackPower + 1);
        target.TakeDamage(damage);
        Console.WriteLine($"  {Name} attacks {target.Name} for {damage} damage!");
    }

    public override string ToString()
    {
        string status = IsAlive() ? $"HP:{Health}" : "DEFEATED";
        return $"{Name} ({GetType().Name}) [{status}]";
    }
}

class Warrior : Character
{
    public int Armor = 5;
    public Warrior(string name, int hp = 120, int atk = 20) : base(name, hp, atk) { }
    public override void TakeDamage(int amount) =>
        base.TakeDamage(Math.Max(amount - Armor, 0));
}

class Mage : Character
{
    public int Mana = 50;
    public Mage(string name, int hp = 80, int atk = 15) : base(name, hp, atk) { }

    public void CastSpell(Character target)
    {
        if (Mana >= 10)
        {
            Mana -= 10;
            int damage = AttackPower * 2;
            target.TakeDamage(damage);
            Console.WriteLine($"  🔥 {Name} casts spell for {damage}! (Mana: {Mana})");
        }
        else Attack(target);
    }
}

class Archer : Character
{
    public int Arrows = 10;
    public Archer(string name, int hp = 90, int atk = 18) : base(name, hp, atk) { }

    public void Shoot(Character target)
    {
        if (Arrows > 0)
        {
            Arrows--;
            int damage = AttackPower + new Random().Next(5, 16);
            target.TakeDamage(damage);
            Console.WriteLine($"  🏹 {Name} shoots for {damage}! (Arrows: {Arrows})");
        }
        else Attack(target);
    }
}
🎓 Instructor Note: Delivery Guidance

Exercise 1 (Shapes) is the classic OOP teaching example — it shows inheritance, method overriding, polymorphism (looping over mixed shapes), and multi-level inheritance (Square extends Rectangle). Exercise 2 (RPG) is the engaging capstone that combines inheritance with polymorphism and composition concepts. The battle simulation with random damage makes every run different — students love running it multiple times. Challenge fast students: add a Healer class that can heal allies instead of attacking, or add experience points that increase stats after each round.

Summary

🎉 Key Takeaways

  • Inheritance lets child classes reuse code from a parent class — "is-a" relationships
  • Syntax: Python class Dog(Animal):, JS class Dog extends Animal, C# class Dog : Animal
  • super() calls the parent's constructor or methods — essential in child constructors
  • Method overriding lets children replace parent behavior (C# requires virtual/override)
  • Polymorphism: same method call, different behavior based on actual type — no if/else type checking needed
  • Type checking: Python isinstance(), JS instanceof, C# is
  • Composition ("has-a") is often more flexible than inheritance — favor it when the relationship isn't a true subtype

🚀 What's Next?

That wraps up Module 6: Object-Oriented Programming! You now have a solid foundation in classes, objects, encapsulation, inheritance, and polymorphism. In Module 7, we tackle something every real program needs: error handling — writing code that gracefully handles the unexpected instead of crashing.

🎯 Quick Check

Question 1: In JavaScript, how do you create a class that inherits from another?

Question 2: What does polymorphism allow you to do?

Question 3: In C#, what keywords are needed to override a parent method?