The Python Coding Stack

The Python Coding Stack

"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

Stephen Gruppetta's avatar
Stephen Gruppetta
Jun 15, 2023

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


Most posts on The Python Coding Stack are available for free and in full. If you like the content and find it useful, you can support the publication through a paid subscription.


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:

# hogwarts_magic.py  import random   class Wizard:     def __init__(self, name, patronus, birth_year):         self.name = name         self.patronus = patronus         self.birth_year = birth_year         self.house = None         self.wand = None         self.skill = 0.2  # 0.0 (bad) to 1.0 (good)      def increase_skill(self, amount):         self.skill += amount         if self.skill > 1.0:             self.skill = 1.0      def assign_wand(self, wand):         self.wand = wand         print(f"{self.name} has a {self.wand} wand.")      def assign_house(self, house):         self.house = house         house.add_member(self)      def cast_spell(self, spell):         if self.wand:             effect = self.wand.cast_spell(spell, self)             if effect:                 print(f"{self.name} cast {effect}!")             else:                 print(f"{self.name} failed to cast {spell.name}!")         else:             print(f"{self.name} has no wand!")   class House:     def __init__(self, name, founder, colours, animal):         self.name = name         self.founder = founder         self.colours = colours         self.animal = animal         self.members = []         self.points = 0      def add_member(self, member):         if member not in self.members:             self.members.append(member)      def remove_member(self, member):         self.members.remove(member)      def update_points(self, points):         self.points += points      def get_house_details(self):         return {             "name": self.name,             "founder": self.founder,             "colours": self.colours,             "animal": self.animal,             "points": self.points         }   class Wand:     def __init__(self, wood, core, length, power=0.5):         self.wood = wood         self.core = core         self.length = length         self.power = power  # 0.0 (weak) to 1.0 (strong)      def cast_spell(self, spell, wizard):         if spell.is_successful(self, wizard):             return spell.effect         return None  # Explicitly return None (for readability)   class Spell:     def __init__(self, name, effect, difficulty):         self.name = name         self.effect = effect         self.difficulty = difficulty  # 0.0 (easy) to 1.0 (hard)      def is_successful(self, wand, wizard):         success_rate = (                 (1 - self.difficulty)                 * wand.power                 * wizard.skill         )         return random.random() < success_rate
copy code

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:

class Professor(Wizard):     def __init__(self, name, patronus, birth_year, subject):         super().__init__(name, patronus, birth_year)         self.subject = subject
copy code

Let's look at the differences between defining the Wizard and Professor classes:

  1. The class name Professor is followed by parentheses containing the name of another class. This shows that Professor inherits from Wizard. It has access to the same data attributes and methods.

  2. The Professor class has its own __init__() method. It includes the parameters self, name, patronus, and birth_year. But there's also subject, which is an additional parameter.

  3. There's a "strange" line of code in the __init__() method that starts with super(). We'll go through what this line does shortly.

  4. Professor has an additional data attribute, .subject. You assign the value from the argument subject to this data attribute.

Let's first look at this short example to understand inheritance at a top level:

>>> class ParentClass: ...     pass  >>> class ChildClass(ParentClass): ...     pass  >>> parent = ParentClass() >>> isinstance(parent, ParentClass) True >>> isinstance(parent, ChildClass) False  >>> child = ChildClass() >>> isinstance(child, ParentClass) True >>> isinstance(child, ChildClass) True

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:

# hogwarts_magic.py  # ...  class Professor(Wizard):     def __init__(self, name, patronus, birth_year, subject):         super().__init__(name, patronus, birth_year)         self.subject = subject      def assess_student(self, student, mark):         # ToDo: Finish writing this method         ...
copy code

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:

class Professor(Wizard):     def __init__(self, name, patronus, birth_year, subject):         super().__init__(name, patronus, birth_year)         self.subject = subject      def assign_wand(self, wand):         super().assign_wand(wand)         self.increase_skill(0.2)      def assess_student(self, student, mark):         # ToDo: Finish writing this method         ...
copy code

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:

# hogwarts_magic.py  # ...  class Student(Wizard):     def __init__(self, name, patronus, birth_year, school_year):         super().__init__(name, patronus, birth_year)         self.school_year = school_year         self.subject_grades = {}      def assign_house_using_sorting_hat(self):         # ToDo: Sorting Hat chooses house and assigns it to student.         # We'll work on this in the final Year                  # The parent class has 'assign_house()', which you'll call         # in this method         ...      def take_exam(self, subject, grade):         self.subject_grades[subject] = grade      def assign_subjects(self, subjects):         self.subject_grades = {subject: None for subject in subjects}  # ...
copy code

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 add Wizard within parentheses after the class keyword and the new class's name, you show that Student inherits from Wizard.

  • You define Student.__init__():

    • The special method has five parameters, including self. There's an additional parameter named school_year compared to the parent class's __init__() method.

    • You call super().__init__() from within Student.__init__() to initialise the object as a Wizard as well as a Student.

    • 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:

# hogwarts_magic.py  # ...  class Professor(Wizard):     # ...          def assess_student(self, student, grade):         if self.subject in student.subject_grades:             print(                 f"{self.name} assessed {student.name} in "                 f"{self.subject}. The grade is {grade}%."             )             student.take_exam(self.subject, grade)         else:             print(                 f"{student.name} doesn't study {self.subject}."             )  # ...
copy code

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:

# making_magic.py  from hogwarts_magic import Wizard, House, Wand, Professor, Student  harry = Student("Harry Potter", "stag", 1980, 1) hermione = Student("Hermione Granger", "otter", 1979, 1)  gryffindor = House(     "Gryffindor",     "Godric Gryffindor",     ["scarlet", "gold"],     "lion", ) slytherin = House(     "Slytherin",     "Salazar Slytherin",     ["green", "silver"],     "serpent" )  snape = Professor("Severus Snape", "doe", 1960, "Potions") snape.assign_wand(Wand("Holly", "Phoenix Feather", 10.5)) # Severus Snape has a #    <hogwarts_magic.Wand object at 0x7fa915302220> wand. snape.assign_house(slytherin)  mcgonagall = Professor(     "Minerva McGonagall", "cat", 1935, "Transfiguration" ) mcgonagall.assign_wand(Wand("Hawthorn", "Unicorn Hair", 11)) # Minerva McGonagall has a #     <hogwarts_magic.Wand object at 0x7fa9153021c0> wand. mcgonagall.assign_house(gryffindor)  harry.assign_subjects(["Potions", "Transfiguration"]) snape.assess_student(harry, 20) # Severus Snape assessed Harry Potter in Potions. The grade is 20%.  snape.assess_student(hermione, 60) # Hermione Granger doesn't study Potions.
copy code

Let's look at all the steps included in this script:

  • You create two Student objects, harry and hermione. Note that in previous versions of this script from previous years, these objects were instances of Wizard. Now that you have a Student subclass, you use this new class.

  • You create two House objects, gryffindor and slytherin.

  • You create two Professor objects, snape and mcgonagall. You call assign_wand() and assign_house() for both instances. Note that the Professor class doesn't have a distinct assign_house() defined for the class. However, it inherits the method from Wizard, 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 to hermione.

  • You call snape.assess_student() twice, once for each student. Since Harry Potter studies Potions, the method assigns a grade to the instance harry. However, the instance hermione 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:

# making_magic.py  # ...  harry.assign_subjects(["Potions", "Transfiguration"]) print(harry.subject_grades) # {'Potions': None, 'Transfiguration': None}  snape.assess_student(harry, 20) # Severus Snape assessed Harry Potter in Potions. The grade is 20%.  print(harry.subject_grades) # {'Potions': 20, 'Transfiguration': None}
copy code

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 the Professor object.

  • snape.assess_student
    We access one of the Professor object's attributes: the method assess_student().

  • snape.assess_student(harry, 20)
    You pass the Student instance harry and the integer 20 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


Most posts on The Python Coding Stack are available for free and in full. If you like the content and find it useful, you can support the publication through a paid subscription


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's turtle is not just for drawing simple shapes

    • The 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.

Discussion about this post

User's avatar
Abhinav Upadhyay's avatar
Abhinav Upadhyay
Jun 15, 2023

Thoroughly enjoyed reading this post. You make it a fun thing to read instead of a cognitive burden.

Reply
Share
1 reply by Stephen Gruppetta
TAFAKARI's avatar
TAFAKARI
Jun 15, 2023Edited

Fantastic as always Stephen, thank you for sharing and teaching - your posts are great refreshers for those with more experience too

Reply
Share
1 reply by Stephen Gruppetta
2 more comments...

No posts

Ready for more?

© 2026 Stephen Gruppetta · Privacy ∙ Terms ∙ Collection notice
Start your SubstackGet the app
Substack is the home for great culture
    This site requires JavaScript to run correctly. Please turn on JavaScript or unblock scripts
    Morty Proxy This is a proxified and sanitized view of the page, visit original site.