"You Have Your Mother's Eyes" • Inheritance in Python Classes (Harry Potter OOP Series #5)
Year 5 at Hogwarts School of Codecraft and Algorithmancy • Inheritance
The Ancient Library shimmered under a sea of astral lanterns, their cool silver light bathing the earnest faces of fifth-year students. In their hands, a book thrummed with the aura of arcane secrets and ancestral wisdom:
Ethereal Heirlooms: Unraveling Inheritance and Harnessing Hierarchies
Last year's expedition had led them through the intricate labyrinth of class interactions, turning these budding wizards into adept Python architects. The hushed whispers of their former lessons lingered in the air, dancing with the dust motes amongst rows of timeworn tomes, serving as a poignant reminder of the path they had trodden.
The forthcoming chapter of their quest held the keys to the grand kingdom of inheritance, a kingdom of castles built upon the foundations of those that came before. Each snippet of code, each method was a stepping stone leading to a more complex citadel of interconnected classes.
Fifth-years, as the Ancient Library's celestial glow dances on the parchment before you, ready yourselves for another deep dive into Python's enchanting world. A chronicle of further learning, unveiling, and mastery is on the horizon.
Students learn about inheritance in the fifth year at Hogwarts School of Codecraft and Algorithmancy.
[Spoiler Alert: There are spoilers from the final Harry Potter book in the next paragraph]
"You have your mother's eyes" is a recurring phrase in the Harry Potter series that seems appropriate as the title for this article about inheritance. Many wizards who knew Harry's mother said that phrase to Harry at some stage. These words were famously also Snape's last words to Harry. Except that…they weren't. Not in the books, in any case. Snape's last words in the book were "Look...at...me". But the reference may have been too subtle for the films. So, in the film, this changed to "Look at me. You have your mother's eyes".
The Curriculum at Hogwarts (Series Overview)
This is the fifth in a series of seven articles, each linked to a year at Hogwarts School of Codecraft and Algorithmancy:
Year 1 is designed to ensure the new students settle into a new school and learn the mindset needed for object-oriented programming
Year 2: Students will start defining classes and learning about data attributes
Year 3: It's time to define methods in the classes
Year 4: Students build on their knowledge and learn about the interaction between classes
Year 5 (this article): Inheritance. The students are ready for this now they're older
Year 6: Students learn about special methods. You may know these as dunder methods
Year 7: It's the final year. Students learn about class methods and static methods
Professors Are Wizards. So Are The Students
Over the past few years at Hogwarts School of Codecraft and Algorithmancy, you built a set of classes to deal with the wizarding world. The classes represent the "things" that describe the situation. So far, you have the following classes:
Wizard
House
Wand
Spell
Each of these classes has its own attributes—data attributes and methods. The data attributes contain the information needed by the objects. The methods give the objects the functionality to do things with the data.
Here's the code with all the classes you have so far:
There are professors and students at Hogwarts School of Codecraft and Algorithmancy. There's different information that's relevant to the two categories. Teachers teach certain subjects and mark exams, for example. Students study several subjects and take exams.
Therefore, we need different classes to represent professors and students. However, professors and students are also wizards. The new classes to represent professors and students should also have the attributes from the Wizard
class you defined in previous years.
It would be wasteful to define two separate classes for professors and students with lots of code in common. That goes against the DRY principle—Don't Repeat Yourself.
Instead, you can create two new classes, Professor
and Student
, that inherit from Wizard
:
All professors are wizards. But not all wizards are professors.
All students are wizards. But not all wizards are students.
When you create a class that inherits from another one, the child class starts off having the same attributes as the parent class. Then, you can add attributes or even change some of the existing ones, as you'll see in this year's curriculum.
Note: several terms can be used to refer to classes when dealing with inheritance. When a class inherits from another class, it can be called a subclass, a derived class or a child class. The class it inherits from can be called a superclass, a base class or a parent class. I'll mostly use the terms 'parent class' and 'child class' as I think they're the most intuitive at this stage.
Create Professor
That Inherits From Wizard
Let's create a Professor
class. I'm also showing the Wizard
class in full in the following code segment:
Let's look at the differences between defining the Wizard
and Professor
classes:
The class name
Professor
is followed by parentheses containing the name of another class. This shows thatProfessor
inherits fromWizard
. It has access to the same data attributes and methods.The
Professor
class has its own__init__()
method. It includes the parametersself
,name
,patronus
, andbirth_year
. But there's alsosubject
, which is an additional parameter.There's a "strange" line of code in the
__init__()
method that starts withsuper()
. We'll go through what this line does shortly.Professor
has an additional data attribute,.subject
. You assign the value from the argumentsubject
to this data attribute.
Let's first look at this short example to understand inheritance at a top level:
The ParentClass
object is an instance of ParentClass
but not of ChildClass
. However, since ChildClass
inherits from ParentClass
, the ChildClass
object is an instance of both classes.
Let's return to the Professor
class in the main code and focus on the following line of code:
super().__init__(name, patronus, birth_year)
This calls the superclass's __init__()
method. In this case, Professor
inherits from Wizard
. Therefore, this line calls Wizard.__init__()
. This call includes the parameters name
, patronus
, and birth_year
, but not subject
since that's not a parameter in the Wizard
class.
An instance of Professor
is also an instance of Wizard
. Therefore, when you initialise a Professor
instance using its __init__()
method, you also initialise it as a Wizard
instance first.
We can have more complex inheritance setups, and super()
will also handle these cases. However, in this series, we won't explore these cases.
Adding and overriding methods in a subclass
A child class starts off with the same attributes as the parent class. You've already seen how to add new data attributes, such as .subject
. You can also add methods to the child class that are not present in the parent class:
The Professor
class now has a method called assess_student()
that's unique to this child class and doesn't exist in the parent class. You'll finish writing this method shortly.
You can add new methods to a child class, as you did with assess_student()
. But you can also change methods that exist in the parent class:
The Wizard
class has its own assign_wand()
method. However, you define another assign_wand()
as part of the Professor
subclass. This new method overrides the one in the parent class. So, when you call this method on a Professor
object, the method defined in Professor
is called.
In this case, the first line in Professor.assign_wand()
is:
super().assign_wand(wand)
You've already seen super()
used. This line calls the assign_wand()
method of the superclass, which is Wizard
. Therefore the method in the child class, Professor
, will first do whatever the method in the parent class does, but it will also perform additional tasks. In this case, it increases the professor's skill.
If you want the new method to be significantly different from the one in the parent class, you don't need to use super()
to call the parent's method. Whether you want to build on the parent's method depends on what you need your method to do.
Create Student
That Inherits from Wizard
Next, you can create the Student
class. Since a student at Hogwarts School of Codecraft and Algorithmancy is also a wizard, this new class also inherits from Wizard
:
You know what to expect. Since Student
inherits from Wizard
, you'll see some similarities with how you dealt with the Professor
class earlier:
You define the class using the line
class Student(Wizard)
. When you addWizard
within parentheses after theclass
keyword and the new class's name, you show thatStudent
inherits fromWizard
.You define
Student.__init__()
:The special method has five parameters, including
self
. There's an additional parameter namedschool_year
compared to the parent class's__init__()
method.You call
super().__init__()
from withinStudent.__init__()
to initialise the object as aWizard
as well as aStudent
.The final lines in
__init__()
create two new data attributes to store the school year and the student's grades. The.subject_grades
data attribute is a dictionary, so it can store the subject name as a key and the test scores as values.
You define three new methods unique to the
Student
class. These methods:Assign a house to the student
Allow the student to take an exam
Assign subjects that the student is taking at school
The first of the three methods is assign_house_using_sorting_hat()
. We'll get back to this method in the final year.
The second method is take_exam()
, which takes the subject name and the grade as arguments. This method updates the .subject_grades
data attribute, which is a dictionary.
The third method, assign_subjects()
, accepts an iterable with the names of the subjects the student is learning and creates the dictionary with all the subjects. Initially, None
is the value associated with all subject names.
Caveat: These methods are not perfect. We could discuss ways of improving them to make them more robust. However, the aim of this series of articles is not to create the perfect software to solve Hogwarts' administrative problems. Instead, we want to ensure we understand how classes work.
Complete Professor.assess_student()
It's time to return to the Professor
class and write the code for the assess_student()
method:
Since you have objects of type Student
, you can pass these objects to Professor.assess_student()
. This is the value associated with the student
parameter in assess_student()
.
You call this method when a professor needs to assess a student. The method first checks whether the subject the professor teaches, self.subject
, is one of the subjects the student is learning—these subjects are stored in the student.subject_grades
dictionary.
If the student is learning the subject, the method calls student.take_exam()
. You pass the subject taught by the professor, self.subject
, and the grade. Recall that self
refers to the Professor
instance in this case since you're using it within the Professor
class. You'll look at this method call in more detail later in this article.
Let's Try These New Classes
It's time to use these new subclasses to see whether they work as intended. You can return to the making_magic.py
script you used in previous years to test these classes:
Let's look at all the steps included in this script:
You create two
Student
objects,harry
andhermione
. Note that in previous versions of this script from previous years, these objects were instances ofWizard
. Now that you have aStudent
subclass, you use this new class.You create two
House
objects,gryffindor
andslytherin
.You create two
Professor
objects,snape
andmcgonagall
. You callassign_wand()
andassign_house()
for both instances. Note that theProfessor
class doesn't have a distinctassign_house()
defined for the class. However, it inherits the method fromWizard
, which is the parent class. The text displayed when you assign a wand to a wizard still has some issues. You'll deal with these in Year 6.Next, you assign two subjects to
harry
. You don't assign any subjects tohermione
.You call
snape.assess_student()
twice, once for each student. Since Harry Potter studies Potions, the method assigns a grade to the instanceharry
. However, the instancehermione
doesn't include"Potions"
as one of the subjects.
I'm using strings to represent subject names in this example. I don't want the code in this series to get out of hand. However, you may want to consider creating a Subject
class to deal with subjects studied at Hogwarts School of Codecraft and Algorithmancy.
Following The Trail (But You Don't Need To)
Let's look at the line in which Snape assesses Harry and gives him a low mark!
snape.assess_student(harry, 20)
Note that this line doesn't have any reference to the subject. This is because, at Hogwarts School of Codecraft and Algorithmancy, each teacher only teaches one subject (the same as the Hogwarts in the original Harry Potter stories.)
However, this line changes Harry Potter's Potions grade:
The value for "Potions"
in harry.subject_grades
changes from None
to 20
after the call to snape.assess_student(harry, 20)
.
Let's follow the trail:
snape.assess_student(harry, 20)
snape
This refers to theProfessor
object.snape.assess_student
We access one of theProfessor
object's attributes: the methodassess_student()
.snape.assess_student(harry, 20)
You pass theStudent
instanceharry
and the integer20
to this method.
You'll recall from earlier years at Hogwarts that this call, snape.assess_student(harry, 20)
, is equivalent to:
Professor.assess_student(snape, harry, 20)
The Professor
instance is passed to the self
parameter, which is the first parameter in all instance methods. Therefore, the method has access to the objects snape
and harry
. And the integer 20
, of course!
This means the method also has access to all the attributes that belong to snape
and harry
, including the subject taught by the professor.
Let's zoom in on the following line of code in the Professor.assess_student()
method:
student.take_exam(self.subject, grade)
In this example, this is equivalent to the following:
harry.take_exam(snape.subject, grade)
Or:
harry.take_exam("Potions", 20)
Our journey now takes us to the Student.take_exam()
method. The call above is equivalent to:
Student.take_exam(harry, "Potions", 20)
The take_exam()
method has the following line of code:
self.subject_grades[subject] = grade
This is equivalent to:
harry.subject_grades["Potions"] = 20
This updates the value associated with the key "Potions"
in the dictionary.
Object-oriented programming allows you to abstract all this functionality within the objects. So, when you call snape.assess_student(harry, 20)
, you don't need to worry about what happens behind the scenes. Whatever is needed for Snape to assess Harry and assign him a grade of 20% will happen out of sight.
Terminology Corner
Subclass or Derived Class or Child Class: A class that inherits from another one
Superclass or Base Class or Parent Class: A class from which other classes inherit
"You have your mother's eyes". Harry Potter clearly inherited his mother's eyes. We all inherit characteristics from our parents, but we also have our own characteristics. We're distinct from our parents. This is similar to inheritance in classes. Child classes inherit attributes from their parent classes, but they can change those attributes and have new ones.
This brings us to the end of Year 5. Enjoy the holidays. Year 6 is special. Literally. You'll learn about special methods (aka dunder methods).
Next article in this series: Year 6 • Time for Something Special • Special Methods in Python Classes
Code in this article uses Python 3.11
Stop Stack
Recently published articles on The Stack:
Casting A Spell • More Interaction Between Classes. Year 4 at Hogwarts School of Codecraft and Algorithmancy • More on Methods and Classes
An Object That Contains Objects • Python's Containers. Containers • Part 4 of the Data Structure Categories Series
Zen and The Art of Python
turtle
Animations • A Step-by-Step Guide. Python'sturtle
is not just for drawing simple shapesThe One About The Taxi Driver, Mappings, and Sequences • A Short Trip to 42 Python Street. How can London cabbies help us learn about mappings and sequences in Python?
Deconstructing Ideas And Constructing Code • Using the Store-Repeat-Decide-Reuse Concept. Starting to code on a blank page • How do you convert your ideas into code?
I've added a new page on The Python Coding Stack's homepage called The Python Series—that's ‘series’ as a plural noun! It's a shame ‘series’ is a word that has the same singular and plural form. For the avid readers, you know that I publish several stand-alone articles, but I also publish articles that are part of series (pl.), such as this article you just finished reading. I'll use this page to keep all series in one place. At the moment, there are links to The Data Structure Category Series and The Harry Potter Object-Oriented Programming Series. I'll add more links to future series here, too.
The Python Coding Stack is now two months old. This is the 14th article I'm publishing and I'm pleased that so many of you are finding these articles useful. There are over 500 of you subscribed to this Substack now, and I know many more read these articles, too. I enjoy writing. And I'm glad that there are people who enjoy reading what I write, too. Thank you.
Most articles will be published in full on the free subscription. However, a lot of effort and time goes into crafting and preparing these articles. If you enjoy the content and find it useful, and if you're in a position to do so, you can become a paid subscriber. In addition to supporting this work, you'll get access to the full archive of articles and some paid-only articles.
Thoroughly enjoyed reading this post. You make it a fun thing to read instead of a cognitive burden.
Fantastic as always Stephen, thank you for sharing and teaching - your posts are great refreshers for those with more experience too