Day 11 of 80

Object-Oriented Programming II: Inheritance

Phase 1: Python Foundation

What You'll Build Today

Today, we are going to build the foundation of a Role-Playing Game (RPG). You will create a system where different types of characters—Warriors, Mages, and Healers—can exist in the same world.

But we aren't just building a game for fun. We are building this to solve a massive structural problem in programming: redundancy.

By the end of today, you will have a working simulation where different characters share core DNA but behave uniquely. Here is what you will learn and why it matters:

* Inheritance: You will learn how to create a "parent" blueprint so you don't have to copy-paste code for every new variation of an object.

* Method Overriding: You will learn how to let a specific object change the way a standard action works (e.g., a Warrior attacks differently than a Mage).

* The super() function: You will learn how to reuse setup code from a parent class while adding new, specific features to a child class.

* Encapsulation: You will learn how to protect sensitive data (like health points) so other parts of your code can't accidentally break your objects.

* Polymorphism: You will learn how to treat a group of different objects exactly the same way, greatly simplifying your code logic.

The Problem

Imagine you are tasked with building a game with two character types: a Warrior and a Mage.

Based on what we learned yesterday about Classes, you might sit down and write code that looks like this:

class Warrior:

def __init__(self, name):

self.name = name

self.health = 100

self.stamina = 50

def move(self):

print(f"{self.name} moves forward.")

def attack(self):

print(f"{self.name} swings a sword!")

self.stamina -= 10

class Mage:

def __init__(self, name):

self.name = name

self.health = 100

self.mana = 60

def move(self):

print(f"{self.name} moves forward.")

def attack(self):

print(f"{self.name} casts a fireball!")

self.mana -= 10

# Testing the code

conan = Warrior("Conan")

gandalf = Mage("Gandalf")

conan.move()

gandalf.move()

Look closely at that code. Do you see the problem?

The problem is duplication.

Both classes have self.name. Both classes have self.health. Both classes have the exact same move() method.

Right now, it is manageable. But imagine your manager comes in and says, "We need to add a level attribute to every character, and we need to change the move() method so it prints coordinates."

You would have to open the Warrior class and change it. Then you would have to open the Mage class and change it. Then the Healer, the Archer, the Thief, and the Paladin.

If you forget to update just one of them, your program becomes inconsistent and buggy. This approach violates the DRY Principle: Don't Repeat Yourself.

There has to be a way to write the common code once and let everyone else just borrow it.

Let's Build It

The solution is Inheritance. We will create a general "Parent" class that holds everything common to all characters, and then create specialized "Child" classes that inherit those features.

Step 1: The Parent Class

First, let's define the Character class. This is our "base" or "parent" class. It contains the logic that everyone shares.

class Character:

def __init__(self, name, health):

self.name = name

self.health = health

def move(self):

print(f"{self.name} moves forward.")

def attack(self):

print(f"{self.name} attacks vaguely!")

# Let's test the base class

npc = Character("Villager", 50)

npc.move()

npc.attack()

Output:
Villager moves forward.

Villager attacks vaguely!

This is a standard class. Nothing new yet.

Step 2: Creating a Child Class (Inheritance)

Now, let's create the Warrior. A Warrior is a Character.

In Python, we pass the parent class name inside the parentheses when defining the child class: class Child(Parent):.

# The Warrior inherits from Character

class Warrior(Character):

pass

# 'pass' just means "do nothing specific here, just use the parent stuff"

conan = Warrior("Conan", 150)

conan.move() # Warrior doesn't have a move method, so it looks at Character

conan.attack() # Warrior doesn't have an attack method, so it looks at Character

Output:
Conan moves forward.

Conan attacks vaguely!

Why this matters: We didn't write __init__ or move inside Warrior. Python looked for them in Warrior, didn't find them, and automatically went up to the parent Character to find them. We just saved ourselves five lines of code.

Step 3: Method Overriding

A Warrior shouldn't "attack vaguely." A Warrior should swing a sword.

We can override the parent's method by defining a method with the exact same name in the child class.

class Warrior(Character):

def attack(self):

print(f"{self.name} swings a giant sword with fury!")

conan = Warrior("Conan", 150)

conan.move() # Uses Parent method (we didn't override it)

conan.attack() # Uses Child method (we DID override it)

Output:
Conan moves forward.

Conan swings a giant sword with fury!

Key Takeaway: When you call a method, Python looks at the Child class first. If it finds the method there, it runs it and stops. If not, it looks at the Parent.

Step 4: The super() Function

Now things get a little tricky. Let's make a Mage.

A Mage needs name and health (like any Character), but they also need mana (magic points).

If we write a new __init__ for the Mage to add mana, we overwrite the Parent's __init__. We don't want to copy-paste the logic for setting name and health. We want to ask the Parent to handle the name and health, and then we will handle the mana.

We use super() to refer to the Parent class.

class Mage(Character):

def __init__(self, name, health, mana):

# Call the parent's __init__ to handle name and health

super().__init__(name, health)

# Now handle the specific Mage stuff

self.mana = mana

def attack(self):

print(f"{self.name} casts a fireball! (Mana: {self.mana})")

gandalf = Mage("Gandalf", 80, 100)

print(f"Name: {gandalf.name}")

print(f"Mana: {gandalf.mana}")

gandalf.attack()

Output:
Name: Gandalf

Mana: 100

Gandalf casts a fireball! (Mana: 100)

Why this matters: super().__init__ allows you to extend the setup process rather than replacing it entirely. You are chaining the initialization logic together.

Step 5: Encapsulation (Private Attributes)

In games (and banking apps), you don't want external code to arbitrarily change important values. Currently, we can do this:

conan.health = -5000

That shouldn't be allowed. We want to protect the health attribute. In Python, we indicate a variable is "private" (internal use only) by adding an underscore _ before the name.

Let's update our Base Class.

class Character:

def __init__(self, name, health):

self.name = name

self._health = health # The underscore signals "Do not touch this directly"

def take_damage(self, amount):

self._health -= amount

if self._health < 0:

self._health = 0

print(f"{self.name} took {amount} damage. Health is now {self._health}")

class Warrior(Character):

def attack(self):

print(f"{self.name} swings a sword!")

# Testing Encapsulation

conan = Warrior("Conan", 150)

# Instead of conan.health = 100, we use the method:

conan.take_damage(20)

Output:
Conan took 20 damage. Health is now 130

By using _health and a take_damage method, we ensure that logic (like checking if health < 0) always runs. We control how the data changes.

Step 6: Polymorphism (Putting it all together)

This is the payoff. Polymorphism means "many forms."

Because Warrior and Mage are both Characters, we can put them in a list and treat them exactly the same, even though they behave differently.

# Create our party

hero1 = Warrior("Conan", 150)

hero2 = Mage("Gandalf", 80, 100)

hero3 = Warrior("Xena", 140)

party = [hero1, hero2, hero3]

print("--- The Battle Begins ---")

for member in party:

# We don't need to know if it's a warrior or mage. # We just know it's a Character, so it has an attack() method.

member.attack()

Output:
--- The Battle Begins ---

Conan swings a sword!

Gandalf casts a fireball! (Mana: 100)

Xena swings a sword!

This loop is incredibly powerful. The code doesn't care what specific class the object is, as long as it inherits from Character.

Now You Try

Here are three extensions to the RPG system. Try to implement them in your code editor.

  • Create a Healer Class:
  • * Create a class Healer that inherits from Character.

    * Give it a mana attribute (use super()).

    * Override attack to print something like "Healer hits with a staff."

    * Add a new method called heal(self, target) that adds health to another character.

  • Add a Status Report:
  • * In the parent Character class, add a method called report_status().

    * It should print: [Name] has [Health] HP.

    * Create a Warrior and a Mage and call report_status() on both to prove the parent method works for children.

  • The Monster Class:
  • * Create a Monster class that inherits from Character.

    * In its __init__, set a default name like "Goblin" if no name is provided.

    * Create a loop where your Warrior attacks the Monster until the Monster's health is 0.

    Challenge Project: The Support Bot

    We are going to switch contexts from RPGs back to AI Agents. You need to build a specialized customer service bot.

    Scenario:

    You have a generic ChatBot that simply echoes messages. You need a SupportBot that can handle specific "help" commands but acts like a normal bot for everything else.

    Requirements:
  • Create a base class ChatBot.
  • * __init__ takes a name.

    * Method respond(message) returns: "[Name] says: [message]".

  • Create a child class SupportBot that inherits from ChatBot.
  • The SupportBot should have a predefined dictionary of help topics (e.g., {'reset': 'Turn it off and on again', 'login': 'Click the top right button'}).
  • Override the respond method in SupportBot:
  • * If the message starts with "help", look up the next word in the dictionary.

    * If the topic exists, return the solution.

    * If the topic doesn't exist, return "I don't know that topic."

    If the message doesn't* start with "help", use super().respond(message) to let the parent class handle it (don't copy-paste the echo logic!). Example Usage:
    bot = SupportBot("TechSupport")
    

    print(bot.respond("Hello")) # Should use parent logic

    print(bot.respond("help reset")) # Should use child logic

    print(bot.respond("help billing")) # Should handle missing key

    Expected Output:
    TechSupport says: Hello
    

    Turn it off and on again

    I don't know that topic.

    Hints:

    * Remember that strings have a .startswith() method.

    * You can .split() a string to separate "help" from the topic.

    * Use super().respond(message) inside the else block of your child class.

    What You Learned

    Today you tackled one of the pillars of Object-Oriented Programming.

    * Inheritance (class Child(Parent)) let you reuse code.

    * Overriding let you customize behavior for specific children.

    * super() let you extend parent functionality without replacing it.

    * Polymorphism let you loop through different objects and treat them uniformly.

    Why This Matters for AI:

    In the real world of GenAI development, you will use this constantly. For example, in the popular library LangChain:

    * There is a base class called BaseRetriever.

    * There are child classes like VectorStoreRetriever and BM25Retriever.

    * They all work differently under the hood, but your main application treats them all the same. You can swap one for the other without breaking your app because they share the same Parent "DNA."

    Tomorrow: We are finally leaving the safety of our local computer. We will learn how to use External Libraries and APIs to make web requests—the first step to connecting your Python code to OpenAI and the internet!