Stage 4: Character Types¶
In this lesson we will learn
- what inheritance is and how child classes get attributes and methods from a parent class
- what polymorphism is and how different classes can use the same method in different ways
- how to refactor code to use child classes without changing its behaviour
- the three types of programming errors: syntax, runtime and logic errors
- how to test and troubleshoot code by comparing expected and actual results
Terminology
- child class – a class based on another class that inherits all of its attributes and methods, also called a subclass or derived class.
- inheritance – the OOP principle of making a new class based on an existing one, so the new class gets the existing class's attributes and methods.
- parent class – the existing class that a child class is based on, also called a superclass or base class.
- override – to replace an inherited method by writing a method with the same name in the child class.
- DRY – short for Don't Repeat Yourself, the rule that we should write code once in one place rather than copying it.
- refactoring – changing how code is written without changing what it does.
- polymorphism – the OOP principle that different classes can have a method with the same name that does different things.
- truthy – describes a value that acts like
Truein anifstatement, such as a non-empty string, a non-zero number or a list with items in it. - falsy – describes a value that acts like
Falsein anifstatement, such asNone,0, an empty string or an empty list. - logic error – a mistake where the program runs without crashing but doesn't do what we meant, so Python gives no warning.
- syntax error – a mistake that breaks the rules of Python, so the program won't run at all.
- runtime error – a mistake that happens while the program is running, when Python tries to do something it can't and crashes with an error message.
Introduction¶
So far we've created a dungeon with several rooms that the player can move between, and filled it with characters the player can interact with. In this stage we'll refine our characters.
Pseudocode¶
- Define two character types:
- Friend
- Enemy
- Change our current characters to one of these types
- Adjust our interactions to allow for the different types
Class diagram¶
The class diagram now shows two new classes: Enemy and Friend.

Both classes have arrows pointing to Character because they are child classes. This means they inherit everything the Character class has. They can also add new features, or replace methods they inherited.
Inheritance
Inheritance in OOP means making a new class that is based on an existing one. It's like a family tree: a child class gets traits from its parent class.
This helps us avoid rewriting the same code, and makes it easier to organise different types of things by what they share and what makes them different.
Enemy class¶
The Enemy class:
- inherits the
name,descriptionandconversationattributes and thedescribe,talkandhugmethods from theCharacterclass - adds the
weaknessattribute - overrides the
Characterfightmethod with its ownfightmethod
Friend class¶
The Friend class:
- inherits the
name,descriptionandconversationattributes and thedescribe,talkandfightmethods from theCharacterclass - overrides the
Characterhugmethod with its ownhugmethod
Why use inheritance?¶
The two child classes end up working like the classes shown in these diagrams.

So why not just make two completely separate classes? Because of the DRY rule: Don't Repeat Yourself.
Both classes use the same describe method, so it's better to write it once in the Character class. Then Friend and Enemy automatically get it. If we ever change describe or talk, we only change it in one place, and both child classes get the updated version.
OOP terminology
Different books and websites sometimes use different words for the same idea:
- a parent class can also be called a superclass or base class
- a child class can also be called a subclass or derived class
Define the character types¶
Create the Friend class¶
Open character.py and add the highlighted code below to create the Friend class.
Code explanation
- line 30 → defines a new class called
Friend. The(Character)shows thatFriendis a child of theCharacterclass. - line 32 → defines the dunder init method, which runs whenever we make a new
Friendobject. - line 34 → runs the parent class's
__init__method first, so aFriendgets all the attributes that aCharacterhas.Characterneeds aname, so we pass thenamethrough.
Create the Enemy class¶
To create the Enemy class, add the highlighted code.
Code explanation
- line 36 → defines the
Enemyclass as a child of theCharacterclass. - line 38 → defines the dunder init method, which runs whenever we make a new
Enemyobject. - line 40 → runs the parent class's
__init__method, so anEnemygets all the usual character attributes. - line 41 → adds a new
weaknessattribute that every enemy has.
Now that we have two character types, we need to change the characters we've created.
Change the character types¶
Return to main.py and change the highlighted code.
Code explanation
- line 4 → imports the
FriendandEnemyclasses instead ofCharacter, because we now create characters as one of these types. - line 24 → creates Ugine as an
Enemy. - line 26 → sets Ugine's weakness, because every enemy has a
weaknessattribute. - line 28 → creates Nigel as a
Friend.
Testing the refactor¶
We've changed how the code is written, but not what it should do. This is called refactoring. Now we need to make sure our changes didn't break anything.
Check that Ugine and Nigel still behave the same as before by filling in this testing table.
| Character | Interaction | Expected result | Actual result |
|---|---|---|---|
| Ugine | talk | ||
| Ugine | hug | ||
| Ugine | fight | ||
| Nigel | talk | ||
| Nigel | hug | ||
| Nigel | fight |
If every expected result matches the actual result, there are no problems. Otherwise, we need to find and fix our mistakes.
Adjust the interactions¶
We want the game to react differently depending on the type of character. We shouldn't hug an enemy, and we shouldn't fight a friend. When different classes respond to the same method in different ways, this is called polymorphism.
Polymorphism
Polymorphism in OOP means different classes can have the same method but do different things with it. It's like asking different people to "work": they all do it, but the way they work depends on their job.
This lets our code treat different objects in the same way, even if they aren't the same type. It makes our programs easier to write, easier to update, and more flexible when things change.
Adjust the hug method¶
Right now, the hug method comes from the Character class, and it always says the character doesn't want to hug us. That's fine for enemies, so we'll leave them. But friends should hug us back, so we need to change the Friend class.
Return to character.py and add the highlighted code.
Code explanation
- line 36 → defines a
hugmethod for theFriendclass. It has the same name as the one inCharacter, so it overrides the old version for all friends. - line 38 → prints a message using the friend's name.
Adjust the fight method¶
Now let's update the fight method for the Enemy class. The fighting system is simple: every enemy has a weakness. If we fight them with their weakness, we win. If we use anything else, we lose.
The highlighted code below creates this mechanic.
Code explanation
- line 47 → defines the
fightmethod for enemies.itemis the weapon the player chooses. - line 49 → checks whether the weapon matches the enemy's weakness.
- line 50 → if it does, prints a winning message…
- line 51 → …and returns
Trueto tell main.py the player won. - lines 52–53 → otherwise prints the losing message…
- line 54 → …and returns
Falseto tell main.py the player lost.
Now we need to update the fight section in main.py so the game uses the new system. Replace that part of the code with the highlighted version below.
PRIMM
- Predict what you think will happen. Be specific.
- Run the program.
- Time to investigate the code. What does each line do?
Code explanation
- line 67 → asks the player which weapon they want to use.
- line 68 → calls the character's
fightmethod, which prints the fight message and returnsTruefor a win orFalsefor a loss. We don't need== True, because theifstatement checks whether the value is truthy or falsy. - line 69 → if the player wins, removes the character from the room…
- lines 70–71 → …otherwise stops the main loop, which ends the game.
Truthy and falsy values
In Python, some values act like True and some act like False in an if statement, even though they aren't True or False. These are called truthy and falsy values.
Truthy values include non-empty strings, non-zero numbers and lists with items in them. Falsy values include None, 0, empty strings, empty lists and False itself.
This matters because when we write something like if current_room.character:, Python checks whether that value is truthy (it exists or has content) or falsy (it's empty or None), and runs the code based on that.
Testing¶
We've changed both the hug and fight methods, so it's time to test. Again, we'll use a testing table and focus on the code we changed.
| Character | Interaction | Weapon | Expected result | Actual result |
|---|---|---|---|---|
| Ugine | fight | cheese | ||
| Ugine | fight | not cheese | ||
| Ugine | hug | - | ||
| Nigel | fight | - | ||
| Nigel | hug | - |
Friend fight error¶
Did you get the following error?
Why did we get this error? Let's read the error message:
- line 2 → the error is on line 68 of main.py.
- line 3 → shows the line of code:
if current_room.character.fight(weapon):. - line 4 → points to the call to
fightas the problem. - line 5 →
fightonly expects one argument (self), but it was given two (selfandweapon).
Let's think about this:
- We have two
fightmethods. Which one caused the problem? - Fighting Ugine worked, but fighting Nigel didn't, so it must be the
fightmethod that friends use. - That method is in character.py, so let's look at it.
Looking closely at the code:
- lines 30–38 → the
Friendclass doesn't have afightmethod, so it uses thefightmethod it inherits fromCharacteron line 26. - line 26 → the
Characterfightmethod only accepts one argument:self. - line 47 → the
Enemyfightmethod accepts two arguments:selfanditem.
We've found the problem, but we need to decide what to fix. We changed main.py so that fighting always uses a weapon, so the simplest fix is to make the Character fight method accept the extra argument too.
Make the highlighted change to character.py.
Code explanation
- line 26 → adds the
itemargument, so every character'sfightmethod can be called with a weapon.
Test again¶
Let's run our testing table again.
| Character | Interaction | Weapon | Expected result | Actual result |
|---|---|---|---|---|
| Ugine | fight | cheese | ||
| Ugine | fight | not cheese | ||
| Ugine | hug | - | ||
| Nigel | fight | - | ||
| Nigel | hug | - |
There's another problem when we fight Nigel, but this time it's different. There's no error message; the program just stops. This type of mistake is called a logic error.
Types of programming errors
There are three main types of programming errors:
- Syntax errors happen when we break the rules of Python. The program won't run at all and shows an error straight away.
- Runtime errors happen while the program is running. Python tries to do something it can't, so it crashes and shows an error. The fight error above was one of these.
- Logic errors happen when the program runs without crashing, but doesn't do what we meant. Python won't warn us, so these are the hardest to find.
This is what the game showed right before the program stopped:
You are in the laboratory
A strange odour hangs in a room filled with unknowable contraptions.
Nigel is here, a burly dwarf with golden beads woven through his beard.
To the west is the armoury
> fight
What will you fight with? > dog
Nigel doesn't want to fight you
Troubleshooting a logic error¶
Fixing logic errors is like being a detective. We follow what the program does step by step to spot where things go wrong.
The problem happens when we fight Nigel, so let's start with the part of the main loop in main.py that handles the fight command.
When we fought Nigel:
- We saw the message
Nigel doesn't want to fight you, so thefightmethod ran on line 68. - Then the game ended, so
runningmust have been set toFalse, which happens on line 71. - Line 71 only runs if the player loses the fight.
- Line 68 decides whether the player won or lost. It calls the
fightmethod and expects aTrueorFalseanswer. - Nigel is a friend, so we need to look at the
fightmethod friends use, which is in theCharacterclass.
Here's the fight method in the Character class in character.py:
Here's the issue: main.py expects fight to return True or False, but the Character fight method doesn't return anything.
In Python, a function without a return statement still returns something: the value None. And None is falsy, so the game thinks the player lost the fight and ends the program.
Let's look at the fight handler in main.py again.
Looking at line 68:
- When we fight Nigel,
current_room.character.fight(weapon)returnsNone. - So the line becomes
if None:, which works the same asif False:. - The program skips the "win" code and goes to the
elseon line 70. - Line 71 sets
runningtoFalse, which ends the game.
Now we know what's happening. To fix it, the Character fight method needs to return True, so line 68 treats it as a win.
Update the method in character.py.
PRIMM
- Predict what you think will happen. Be specific.
- Run the program.
- Time to investigate the code. What does each line do?
Code explanation
- line 29 → returns
Truewhen we fight a character that isn't an enemy, so line 71 of main.py doesn't run and the game keeps going.
Third time lucky?¶
Let's make sure the logic error is solved. Complete the testing table again.
| Character | Interaction | Weapon | Expected result | Actual result |
|---|---|---|---|---|
| Ugine | fight | cheese | ||
| Ugine | fight | not cheese | ||
| Ugine | hug | - | ||
| Nigel | fight | - | ||
| Nigel | hug | - |
Could you hug Nigel after fighting him? Probably not. The game treated the fight as a win, so it removed Nigel from the room. Friends shouldn't disappear when we "fight" them.
In main.py, change the highlighted code below.
PRIMM
- Predict what you think will happen. Be specific.
- Run the program. Check that we can fight Nigel and still hug him afterwards.
- Time to investigate the code. What does each line do?
Code explanation
- line 69 → checks whether the character in the room is an
Enemy.isinstanceisTrueonly when the object was created from theEnemyclass (or one of its child classes)… - line 70 → …and only then removes the character from the room.
Exercises¶
Now it's time to make.
Exercise 1¶
Can you turn each character you've added into a Friend or an Enemy? If they're an enemy, don't forget to give them a weakness. Test each one with a testing table.