38  Class Inheritance: Creating Character Specializations

In our previous lessons, we learned how to create classes and add methods to them. Today, we’ll explore inheritance - a powerful feature that lets us create new classes based on existing ones. Just as a Paladin is a special type of Warrior who also has holy powers, we can create specialized classes that build upon more basic ones.

38.1 What is Inheritance?

Inheritance allows us to create a new class that’s a special version of an existing class. The new class (called the child or subclass) gets all the attributes and methods of the original class (called the parent or superclass), and we can add new ones or modify existing ones.

Let’s start with a basic Character class and create specialized versions of it:

class Character:
    def __init__(self, name, health, strength):
        self.name = name
        self.health = health
        self.strength = strength
    
    def attack(self, target):
        print(f"{self.name} attacks {target} for {self.strength} damage!")

class Warrior(Character):  # Warrior inherits from Character
    def __init__(self, name, health, strength, weapon):
        # First, set up the basic character attributes
        super().__init__(name, health, strength)
        # Then add warrior-specific attribute
        self.weapon = weapon
    
    def battle_cry(self):
        print(f"{self.name} shouts: For glory!")

# Create a warrior
hero = Warrior("Parzival", 100, 15, "Excalibur")

# Use both Character and Warrior methods
hero.attack("Dragon")  # From Character class
hero.battle_cry()      # From Warrior class

This will output:

Parzival attacks Dragon for 15 damage!
Parzival shouts: For glory!

38.2 Creating Different Character Types

Let’s create several specialized character classes:

class Character:
    def __init__(self, name, health, strength):
        self.name = name
        self.health = health
        self.strength = strength
    
    def attack(self, target):
        print(f"{self.name} attacks {target} for {self.strength} damage!")

class Warrior(Character):
    def __init__(self, name, health, strength, weapon):
        super().__init__(name, health, strength)
        self.weapon = weapon
    
    def battle_cry(self):
        print(f"{self.name} shouts: For glory!")

class Mage(Character):
    def __init__(self, name, health, strength, mana):
        super().__init__(name, health, strength)
        self.mana = mana
    
    def cast_spell(self, spell, target):
        if self.mana >= 10:
            print(f"{self.name} casts {spell} at {target}!")
            self.mana -= 10
        else:
            print(f"{self.name} is out of mana!")

class Archer(Character):
    def __init__(self, name, health, strength, arrows):
        super().__init__(name, health, strength)
        self.arrows = arrows
    
    def shoot(self, target):
        if self.arrows > 0:
            print(f"{self.name} shoots an arrow at {target}!")
            self.arrows -= 1
        else:
            print(f"{self.name} is out of arrows!")

# Create different character types
warrior = Warrior("Parzival", 100, 15, "Excalibur")
mage = Mage("Merlin", 80, 5, 100)
archer = Archer("Robin", 90, 10, 20)

# Try out their abilities
warrior.attack("Dragon")        # From Character class
warrior.battle_cry()           # From Warrior class
mage.cast_spell("Fireball", "Dragon")  # From Mage class
archer.shoot("Dragon")         # From Archer class

38.3 Overriding Parent Methods

Sometimes we want a child class to do something differently than its parent class. We can override methods to do this:

class Character:
    def __init__(self, name, health, strength):
        self.name = name
        self.health = health
        self.strength = strength
    
    def attack(self, target):
        print(f"{self.name} attacks {target} for {self.strength} damage!")

class Warrior(Character):
    def __init__(self, name, health, strength, weapon):
        super().__init__(name, health, strength)
        self.weapon = weapon
    
    def attack(self, target):  # Override the attack method
        weapon_bonus = 5
        total_damage = self.strength + weapon_bonus
        print(f"{self.name} attacks {target} with {self.weapon}")
        print(f"Dealing {total_damage} damage!")

# Compare the different attacks
character = Character("Villager", 50, 5)
warrior = Warrior("Parzival", 100, 15, "Excalibur")

character.attack("Training Dummy")
warrior.attack("Training Dummy")

This will output:

Villager attacks Training Dummy for 5 damage!
Parzival attacks Training Dummy with Excalibur
Dealing 20 damage!

38.4 Using super() in Methods

The super() function lets us call methods from the parent class. This is useful when we want to extend, rather than completely replace, a parent’s method:

class Character:
    def __init__(self, name, health, strength):
        self.name = name
        self.health = health
        self.strength = strength
    
    def level_up(self):
        self.health += 10
        self.strength += 2
        print(f"{self.name} reaches a new level!")
        print(f"Health increased to {self.health}")
        print(f"Strength increased to {self.strength}")

class Mage(Character):
    def __init__(self, name, health, strength, mana):
        super().__init__(name, health, strength)
        self.mana = mana
    
    def level_up(self):
        # First, do the normal level up stuff
        super().level_up()
        # Then add mage-specific improvements
        self.mana += 20
        print(f"Mana increased to {self.mana}")

# Create and level up a mage
merlin = Mage("Merlin", 80, 5, 100)
merlin.level_up()

38.5 Practice Time: Class Inheritance

Now it’s your turn to work with inheritance! Try these challenges:

  1. Create a Weapon base class and several specialized weapon classes (Sword, Bow, Staff) that inherit from it:
class Weapon:
    def __init__(self, name, damage):
        # Your code here
        pass
    
    def attack(self):
        # Your code here
        pass

class Sword(Weapon):
    # Your code here
    pass

class Bow(Weapon):
    # Your code here
    pass
  1. Make a Spell base class and create different types of spells that inherit from it:
class Spell:
    def __init__(self, name, mana_cost):
        # Your code here
        pass
    
    def cast(self, caster, target):
        # Your code here
        pass

class FireSpell(Spell):
    # Your code here
    pass

class IceSpell(Spell):
    # Your code here
    pass
  1. Create a Monster base class and several specific monster types:
class Monster:
    def __init__(self, name, health, damage):
        # Your code here
        pass

class Dragon(Monster):
    # Your code here
    pass

class Troll(Monster):
    # Your code here
    pass

38.6 Common Bugs to Watch Out For

As you work with inheritance, be wary of these common pitfalls:

  1. Forgetting super().__init__(): Always call the parent’s __init__ method in your child class constructors.

    class Wrong(Parent):
        def __init__(self, name):
            self.other = "Something"  # Parent's __init__ never called!
    
    class Right(Parent):
        def __init__(self, name):
            super().__init__(name)  # Call parent first
            self.other = "Something"
  2. Method Resolution Order: Python looks for methods in the child class first, then the parent. Be aware of this when overriding methods.

  3. Accessing Parent Methods: Use super() to access parent methods, don’t try to call them directly through the parent class name.

  4. Multiple Inheritance Complexity: While Python supports inheriting from multiple classes, it’s usually better to stick to single inheritance when learning.

  5. Overriding Methods Incorrectly: When overriding methods, make sure the parameters match the parent class method.

38.7 Conclusion and Further Resources

You’ve now learned about inheritance, one of the most powerful features of object-oriented programming. With inheritance, you can create hierarchies of related classes, making your code more organized and reusable.

To learn more about Python inheritance, check out these resources:

  1. Python’s Official Tutorial on Inheritance
  2. Real Python’s Guide to Inheritance in Python
  3. W3Schools Python Inheritance

In our next lesson, we’ll explore some advanced class concepts including class methods, static methods, and properties. Keep practicing with inheritance, and soon you’ll be creating complex class hierarchies with ease!