Python — OOP Part 5: Inheritance and Subclass
In this lesson we cover class inheritance and subclasses.
As we’ve seen, classes use namespaces to manage data efficiently and embody the DRY (Don’t Repeat Yourself) principle by eliminating duplicated code. Inheritance serves the same purpose: you take an already-defined type, extend or modify it as needed, and avoid repeating code. Let’s walk through this with an example by building game character classes. The characters we want are:
| Attribute | Value |
|---|---|
| Class | Hero |
| Rank | Hero |
| Life | 300 |
| Size | Big |
| Description | A high-rank character that summons lower-rank characters. |
| Attribute | Value |
|---|---|
| Class | Goblin |
| Rank | Soldier |
| Life | 100 |
| Size | Small |
| Description | A low-rank character summoned by higher-rank characters. |
First, we’ll create a Unit class that holds the attributes every character will share — the base of all classes — and then make a Goblin subclass that inherits all of Unit’s attributes.
class Unit:
def __init__(self, rank, size, life):
self.name = self.__class__.__name__
self.rank = rank
self.size = size
self.life = life
def show_status(self):
print('Name: {}'.format(self.name))
print('Rank: {}'.format(self.rank))
print('Size: {}'.format(self.size))
print('Life: {}'.format(self.life))
class Goblin(Unit):
pass
goblin_1 = Goblin('Soldier', 'Small', 100)
goblin_1.show_status()Name: Goblin
Rank: Soldier
Size: Small
Life: 100Even though the Goblin subclass didn’t define any methods or attributes of its own, it inherited the show_status method from Unit and was able to use it. So does the Goblin class actually have show_status in its own namespace? Let’s check.
class Unit:
def __init__(self, rank, size, life):
self.name = self.__class__.__name__
self.rank = rank
self.size = size
self.life = life
def show_status(self):
print('Name: {}'.format(self.name))
print('Rank: {}'.format(self.rank))
print('Size: {}'.format(self.size))
print('Life: {}'.format(self.life))
class Goblin(Unit):
pass
goblin_1 = Goblin('Soldier', 'Small', 100)
print(Goblin.__dict__){'__doc__': None, '__module__': '__main__'}As the result shows, the Goblin class doesn’t actually carry show_status itself. So how did the call work? Let’s use the help() function to see how Goblin found show_status.
class Unit:
def __init__(self, rank, size, life):
self.name = self.__class__.__name__
self.rank = rank
self.size = size
self.life = life
def show_status(self):
print('Name: {}'.format(self.name))
print('Rank: {}'.format(self.rank))
print('Size: {}'.format(self.size))
print('Life: {}'.format(self.life))
class Goblin(Unit):
pass
print(help(Goblin))Help on class Goblin in module __main__:
class Goblin(Unit)
| Goblin(rank, size, life)
|
| Method resolution order:
| Goblin
| Unit
| builtins.object
|
| Methods inherited from Unit:
|
| __init__(self, rank, size, life)
| Initialize self. See help(type(self)) for accurate signature.
|
| show_status(self)
|
| ----------------------------------------------------------------------
| Data descriptors inherited from Unit:
|
| __dict__
| dictionary for instance variables (if defined)
|
| __weakref__
| list of weak references to the object (if defined)
None
| Method resolution order:
| Goblin
| Unit
| builtins.objectThat output shows the order in which Goblin resolves names: first its own namespace, then the parent class Unit’s namespace, and finally the top-level built-in object class’s namespace. So even __init__(), which must run when an instance is created, gets looked up in the parent class if it isn’t found in the subclass’s namespace.
| Methods inherited from Unit:
|
| __init__(self, rank, size, life)
| Initialize self. See help(type(self)) for accurate signature.
|
| show_status(self)The output above shows __init__() and show_status() were inherited from Unit. The subclass doesn’t have them in its own namespace, but it references them through the parent class’s namespace.
Let’s use dir() to see what Goblin can reference.
class Unit:
def __init__(self, rank, size, life):
self.name = self.__class__.__name__
self.rank = rank
self.size = size
self.life = life
def show_status(self):
print('Name: {}'.format(self.name))
print('Rank: {}'.format(self.rank))
print('Size: {}'.format(self.size))
print('Life: {}'.format(self.life))
class Goblin(Unit):
pass
print(dir(Goblin))['__class__', '__delattr__', '__dict__', '__dir__', '__doc__', '__eq__', '__format__', '__ge__', '__getattribute__', '__gt__', '__hash__', '__init__', '__init_subclass__', '__le__', '__lt__', '__module__', '__ne__', '__new__', '__reduce__', '__reduce_ex__', '__repr__', '__setattr__', '__sizeof__', '__str__', '__subclasshook__', '__weakref__', 'show_status']Now let’s add a new attribute, “attack type”, to the Goblin class.
class Unit:
def __init__(self, rank, size, life):
self.name = self.__class__.__name__
self.rank = rank
self.size = size
self.life = life
def show_status(self):
print('Name: {}'.format(self.name))
print('Rank: {}'.format(self.rank))
print('Size: {}'.format(self.size))
print('Life: {}'.format(self.life))
class Goblin(Unit):
def __init__(self, rank, size, life, attack_type):
self.name = self.__class__.__name__
self.rank = rank
self.size = size
self.life = life
self.attack_type = attack_type
def show_status(self):
print('Name: {}'.format(self.name))
print('Rank: {}'.format(self.rank))
print('Size: {}'.format(self.size))
print('Life: {}'.format(self.life))
print('Attack type: {}'.format(self.attack_type))
goblin_1 = Goblin('Soldier', 'Small', 100, 'Melee')
goblin_1.show_status()Name: Goblin
Rank: Soldier
Size: Small
Life: 100
Attack type: MeleeWe redefined __init__() and show_status() inside the subclass to add the new attribute and behavior — but we copied all the code from Unit. Writing it this way completely defeats the point of using classes and is a textbook anti-pattern.
Redefining a parent class’s method inside a subclass like this is called method override, and in such cases we use the super() function to call the method already defined in the parent class so we don’t repeat the same code. Let’s reduce the duplication using super().
class Unit:
def __init__(self, rank, size, life):
self.name = self.__class__.__name__
self.rank = rank
self.size = size
self.life = life
def show_status(self):
print('Name: {}'.format(self.name))
print('Rank: {}'.format(self.rank))
print('Size: {}'.format(self.size))
print('Life: {}'.format(self.life))
class Goblin(Unit):
def __init__(self, rank, size, life, attack_type):
super(Goblin, self).__init__(rank, size, life)
self.attack_type = attack_type
def show_status(self):
super(Goblin, self).show_status()
print('Attack type: {}'.format(self.attack_type))
goblin_1 = Goblin('Soldier', 'Small', 100, 'Melee')
goblin_1.show_status()Name: Goblin
Rank: Soldier
Size: Small
Life: 100
Attack type: MeleeHow about that? Much simpler, right? This is method override.
Now let’s add an attack method and a damage attribute to the Goblin class. Then we’ll create a SphereGoblin class that inherits from Goblin and adds a sphere_type attribute.
class Unit:
def __init__(self, rank, size, life):
self.name = self.__class__.__name__
self.rank = rank
self.size = size
self.life = life
def show_status(self):
print('Name: {}'.format(self.name))
print('Rank: {}'.format(self.rank))
print('Size: {}'.format(self.size))
print('Life: {}'.format(self.life))
class Goblin(Unit):
# add damage attribute
def __init__(self, rank, size, life, attack_type, damage):
super(Goblin, self).__init__(rank, size, life)
self.attack_type = attack_type
self.damage = damage
def show_status(self):
super(Goblin, self).show_status()
print('Attack type: {}'.format(self.attack_type))
# overridden method
print('Damage: {}'.format(self.damage))
# add attack method
def attack(self):
print('[{}] attacks! Damage to opponent ({})'.format(self.name, self.damage))
class SphereGoblin(Goblin):
def __init__(self, rank, size, life, attack_type, damage, sphere_type):
super(SphereGoblin, self).__init__(rank, size, life, attack_type, damage)
self.sphere_type = sphere_type
def show_status(self):
super(SphereGoblin, self).show_status()
print('Spear type: {}'.format(self.sphere_type))
sphere_goblin_1 = SphereGoblin('Soldier', 'Small', 100, 'Ranged', 10, 'Long Spear')
sphere_goblin_1.show_status()Name: SphereGoblin
Rank: Soldier
Size: Small
Life: 100
Attack type: Ranged
Damage: 10
Spear type: Long SpearSphereGoblin inherited every attribute and method from its parent Goblin and its grandparent Unit.
Now let’s make a Hero class that commands the goblin characters.
class Unit:
def __init__(self, rank, size, life):
self.name = self.__class__.__name__
self.rank = rank
self.size = size
self.life = life
def show_status(self):
print('Name: {}'.format(self.name))
print('Rank: {}'.format(self.rank))
print('Size: {}'.format(self.size))
print('Life: {}'.format(self.life))
class Goblin(Unit):
# add damage attribute
def __init__(self, rank, size, life, attack_type, damage):
super(Goblin, self).__init__(rank, size, life)
self.attack_type = attack_type
self.damage = damage
def show_status(self):
super(Goblin, self).show_status()
print('Attack type: {}'.format(self.attack_type))
# overridden method
print('Damage: {}'.format(self.damage))
# add attack method
def attack(self):
print('[{}] attacks! Damage to opponent ({})'.format(self.name, self.damage))
class SphereGoblin(Goblin):
def __init__(self, rank, size, life, attack_type, damage, sphere_type):
super(SphereGoblin, self).__init__(rank, size, life, attack_type, damage)
self.sphere_type = sphere_type
def show_status(self):
super(SphereGoblin, self).show_status()
print('Spear type: {}'.format(self.sphere_type))
class Hero(Unit):
def __init__(self, rank, size, life, goblins=None):
super(Hero, self).__init__(rank, size, life)
if goblins is None:
self.goblins = []
else:
self.goblins = goblins
def show_own_goblins(self):
num_of_goblins = len([x for x in self.goblins if isinstance(x, Goblin)])
num_of_sphere_goblins = len([x for x in self.goblins if isinstance(x, SphereGoblin)])
print('The hero currently owns {} goblins and {} spear goblins.'.format(num_of_goblins, num_of_sphere_goblins))
def make_goblins_attack(self):
for goblin in self.goblins:
goblin.attack()
# create goblin objects
goblin_1 = Goblin('Soldier', 'Small', 100, 'Melee', 15)
goblin_2 = Goblin('Soldier', 'Small', 100, 'Melee', 15)
sphere_goblin_1 = SphereGoblin('Soldier', 'Small', 100, 'Ranged', 10, 'Long Spear')
# create hero object and assign goblin objects
hero_1 = Hero('Hero', 'Big', 300, [goblin_1, goblin_2, sphere_goblin_1])
hero_1.show_own_goblins()
hero_1.make_goblins_attack()The hero currently owns 3 goblins and 1 spear goblins.
[Goblin] attacks! Damage to opponent (15)
[Goblin] attacks! Damage to opponent (15)
[SphereGoblin] attacks! Damage to opponent (10)We made a Hero class that inherits from the top-level Unit class and overrode __init__(). We also added a method to display the number of goblins it owns and a method to make those goblins attack. Now let’s add methods to add and remove goblins.
class Unit:
def __init__(self, rank, size, life):
self.name = self.__class__.__name__
self.rank = rank
self.size = size
self.life = life
def show_status(self):
print('Name: {}'.format(self.name))
print('Rank: {}'.format(self.rank))
print('Size: {}'.format(self.size))
print('Life: {}'.format(self.life))
class Goblin(Unit):
# add damage attribute
def __init__(self, rank, size, life, attack_type, damage):
super(Goblin, self).__init__(rank, size, life)
self.attack_type = attack_type
self.damage = damage
def show_status(self):
super(Goblin, self).show_status()
print('Attack type: {}'.format(self.attack_type))
# overridden method
print('Damage: {}'.format(self.damage))
# add attack method
def attack(self):
print('[{}] attacks! Damage to opponent ({})'.format(self.name, self.damage))
class SphereGoblin(Goblin):
def __init__(self, rank, size, life, attack_type, damage, sphere_type):
super(SphereGoblin, self).__init__(rank, size, life, attack_type, damage)
self.sphere_type = sphere_type
def show_status(self):
super(SphereGoblin, self).show_status()
print('Spear type: {}'.format(self.sphere_type))
class Hero(Unit):
def __init__(self, rank, size, life, goblins=None):
super(Hero, self).__init__(rank, size, life)
if goblins is None:
self.goblins = []
else:
self.goblins = goblins
def show_own_goblins(self):
num_of_goblins = len([x for x in self.goblins if isinstance(x, Goblin)])
num_of_sphere_goblins = len([x for x in self.goblins if isinstance(x, SphereGoblin)])
print('The hero currently owns {} goblins and {} spear goblins.'.format(
num_of_goblins - num_of_sphere_goblins, num_of_sphere_goblins
))
def make_goblins_attack(self):
for goblin in self.goblins:
goblin.attack()
def add_goblins(self, new_goblins):
for goblin in new_goblins:
if goblin not in self.goblins:
self.goblins.append(goblin)
else:
print('That goblin has already been added.')
def remove_goblins(self, old_goblins):
for goblin in old_goblins:
try:
self.goblins.remove(goblin)
except:
print("You don't own {}.".format(goblin))
# create goblin objects
goblin_1 = Goblin('Soldier', 'Small', 100, 'Melee', 15)
goblin_2 = Goblin('Soldier', 'Small', 100, 'Melee', 15)
sphere_goblin_1 = SphereGoblin('Soldier', 'Small', 100, 'Ranged', 10, 'Long Spear')
# create hero object and assign goblin objects
hero_1 = Hero('Hero', 'Big', 300, [goblin_1, goblin_2, sphere_goblin_1])
# create new goblins
goblin_3 = Goblin('Soldier', 'Small', 100, 'Melee', 20)
sphere_goblin_2 = SphereGoblin('Soldier', 'Small', 100, 'Ranged', 5, 'Long Spear')
print('# before adding new goblins')
hero_1.show_own_goblins()
hero_1.make_goblins_attack()
# add new goblins
hero_1.add_goblins([goblin_3, sphere_goblin_2])
print('\n# after adding new goblins')
hero_1.show_own_goblins()
hero_1.make_goblins_attack()
# remove the added goblins
hero_1.remove_goblins([goblin_3, sphere_goblin_2])
print('\n# after removing the added goblins')
hero_1.show_own_goblins()
hero_1.make_goblins_attack()# before adding new goblins
The hero currently owns 2 goblins and 1 spear goblins.
[Goblin] attacks! Damage to opponent (15)
[Goblin] attacks! Damage to opponent (15)
[SphereGoblin] attacks! Damage to opponent (10)
# after adding new goblins
The hero currently owns 3 goblins and 2 spear goblins.
[Goblin] attacks! Damage to opponent (15)
[Goblin] attacks! Damage to opponent (15)
[SphereGoblin] attacks! Damage to opponent (10)
[Goblin] attacks! Damage to opponent (20)
[SphereGoblin] attacks! Damage to opponent (5)
# after removing the added goblins
The hero currently owns 2 goblins and 1 spear goblins.
[Goblin] attacks! Damage to opponent (15)
[Goblin] attacks! Damage to opponent (15)
[SphereGoblin] attacks! Damage to opponent (10)Let’s try adding a goblin that’s already been added, and removing one we don’t own, to trigger the error messages.
class Unit:
def __init__(self, rank, size, life):
self.name = self.__class__.__name__
self.rank = rank
self.size = size
self.life = life
def show_status(self):
print('Name: {}'.format(self.name))
print('Rank: {}'.format(self.rank))
print('Size: {}'.format(self.size))
print('Life: {}'.format(self.life))
class Goblin(Unit):
# add damage attribute
def __init__(self, rank, size, life, attack_type, damage):
super(Goblin, self).__init__(rank, size, life)
self.attack_type = attack_type
self.damage = damage
def show_status(self):
super(Goblin, self).show_status()
print('Attack type: {}'.format(self.attack_type))
# overridden method
print('Damage: {}'.format(self.damage))
# add attack method
def attack(self):
print('[{}] attacks! Damage to opponent ({})'.format(self.name, self.damage))
class SphereGoblin(Goblin):
def __init__(self, rank, size, life, attack_type, damage, sphere_type):
super(SphereGoblin, self).__init__(rank, size, life, attack_type, damage)
self.sphere_type = sphere_type
def show_status(self):
super(SphereGoblin, self).show_status()
print('Spear type: {}'.format(self.sphere_type))
class Hero(Unit):
def __init__(self, rank, size, life, goblins=None):
super(Hero, self).__init__(rank, size, life)
if goblins is None:
self.goblins = []
else:
self.goblins = goblins
def show_own_goblins(self):
num_of_goblins = len([x for x in self.goblins if isinstance(x, Goblin)])
num_of_sphere_goblins = len([x for x in self.goblins if isinstance(x, SphereGoblin)])
print('The hero currently owns {} goblins and {} spear goblins.'.format(num_of_goblins, num_of_sphere_goblins))
def make_goblins_attack(self):
for goblin in self.goblins:
goblin.attack()
def add_goblins(self, new_goblins):
for goblin in new_goblins:
if goblin not in self.goblins:
self.goblins.append(goblin)
else:
print('That goblin has already been added.')
def remove_goblins(self, old_goblins):
for goblin in old_goblins:
try:
self.goblins.remove(goblin)
except:
print("That's a goblin you don't own.")
# create goblin objects
goblin_1 = Goblin('Soldier', 'Small', 100, 'Melee', 15)
goblin_2 = Goblin('Soldier', 'Small', 100, 'Melee', 15)
sphere_goblin_1 = SphereGoblin('Soldier', 'Small', 100, 'Ranged', 10, 'Long Spear')
# create hero object and assign goblin objects
hero_1 = Hero('Hero', 'Big', 300, [goblin_1, goblin_2, sphere_goblin_1])
# create new goblins
goblin_3 = Goblin('Soldier', 'Small', 100, 'Melee', 20)
sphere_goblin_2 = SphereGoblin('Soldier', 'Small', 100, 'Ranged', 5, 'Long Spear')
print('# before adding new goblins')
hero_1.show_own_goblins()
hero_1.make_goblins_attack()
# add new goblins
hero_1.add_goblins([goblin_3, sphere_goblin_2])
print('\n# after adding new goblins')
hero_1.show_own_goblins()
hero_1.make_goblins_attack()
# remove the added goblins
hero_1.remove_goblins([goblin_3, sphere_goblin_2])
print('\n# after removing the added goblins')
hero_1.show_own_goblins()
hero_1.make_goblins_attack()
# create a goblin not owned by the hero
goblin_4 = Goblin('Soldier', 'Small', 100, 'Melee', 20)
# add an already-owned goblin
print('\n# error messages')
hero_1.add_goblins([goblin_1])
hero_1.remove_goblins([goblin_4])# before adding new goblins
The hero currently owns 3 goblins and 1 spear goblins.
[Goblin] attacks! Damage to opponent (15)
[Goblin] attacks! Damage to opponent (15)
[SphereGoblin] attacks! Damage to opponent (10)
# after adding new goblins
The hero currently owns 5 goblins and 2 spear goblins.
[Goblin] attacks! Damage to opponent (15)
[Goblin] attacks! Damage to opponent (15)
[SphereGoblin] attacks! Damage to opponent (10)
[Goblin] attacks! Damage to opponent (20)
[SphereGoblin] attacks! Damage to opponent (5)
# after removing the added goblins
The hero currently owns 3 goblins and 1 spear goblins.
[Goblin] attacks! Damage to opponent (15)
[Goblin] attacks! Damage to opponent (15)
[SphereGoblin] attacks! Damage to opponent (10)
# error messages
That goblin has already been added.
That's a goblin you don't own.The error messages print fine.
That wraps up this lesson. The explanation ran a bit long, but hopefully you now have a solid understanding of class inheritance and how to build subclasses. With programming, reading books and articles matters, but what matters even more is actually writing code, running into problems, searching for answers or asking for help, and learning by doing. Build lots of classes, and think hard about what kinds of data are good candidates to model as classes.
The next post covers the magic methods that classes provide.