Time for Something Special • Special Methods in Python Classes (Harry Potter OOP Series #6)
Year 6 at Hogwarts School of Codecraft and Algorithmancy • Special Methods (aka Dunder Methods)
The lanterns of the Ancient Library glistened, casting moonlit enchantments as the diligent sixth-year students embarked on yet another magical journey. The whispers of enchanted books and scrolls filled the air, opening their well-worn pages to reveal:
The Art of Invocation: The Magic of Python's Special Methods Awakened
Their previous lessons in inheritance's mysterious domain had granted them insights into powerful lineages, solidifying their position as skilled Python practitioners.
This new chapter of knowledge uncovers Python's special methods, concealed beneath the vast tapestry of the language. Each piece of code and method would reveal a new facet, allowing the students to harness Python's inherent versatility.
Sixth-years gather by the Ancient Library's luminescent light and prepare to embark on another voyage, ready to unfurl the scrolls that conceal the enigmatic secrets of Python's special methods.
Students learn about special methods in the sixth year at Hogwarts School of Codecraft and Algorithmancy.
The Curriculum at Hogwarts (Series Overview)
This is the sixth 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: Inheritance. The students are ready for this now they're older
Year 6 (this article): 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
It's been a long journey. The fresh-faced first years you met in Year 1 have grown up. But there's still a bit to go before they graduate as OOP wizards and witches. This year is special.
Here's a reminder of the classes you worked on in the previous articles. These are defined in the script hogwarts_magic.py
. You can scroll past this code segment if you just completed the previous article in the series:
There's a Wizard
class and the two classes which inherit from it, Professor
and Student
. You also define House
, Wand
, and Spell
. Each class has several data attributes to store information about each instance and methods to enable you to perform actions using the instances.
But there are other types of behaviour you may want to deal with.
Who's the Greater Wizard or Witch?
Let's create a couple of Student
instances. You can try to compare them using the greater than >
operator. You can test this in the script making_magic.py
:
But this code raises an error:
Traceback (most recent call last):
File ... line 6, in <module>
print(hermione > malfoy)
TypeError: '>' not supported between instances of 'Student' and 'Student'
You cannot use the greater than >
operator with instances of type Student
. Not yet.
This is not that surprising. What does "is Hermione greater than Malfoy" mean, after all? Are you comparing height? Age? Skill? Or perhaps something else?
It's not yet clear what "greater than" means. It hasn't been defined yet.
You can give "greater than" a meaning by defining a method called __gt__()
, which stands for, you guessed it, "greater than". Comparing the skill of two students seems to be appropriate. So you can define __gt__()
for the Student
class.
But before you do so, let's take a step back and look at the bigger picture. Professors can also be compared in the same way. A wizard is "greater than" another wizard if their skill is higher. This is the case for students and professors.
You explored inheritance in the previous article in this series. Since both the Student
and Professor
classes inherit from Wizard
, you can define the __gt__()
method for Wizard
. Student
and Professor
will inherit this method from the parent class:
A quick reminder: the ‘copy code’ link at the bottom of the code sections includes the whole script and not just the truncated section shown in the article.
The method __gt__()
has two parameters. You've already used self
as the first parameter in other methods. This means the instance itself is passed to the method. The second argument is usually called other
and refers to another instance of the class. The method returns a Boolean. In this case, you compare the .skill
data attributes for the two instances.
You can try to rerun making_magic.py
now that you defined this new method. This script is unchanged from the earlier version:
This code no longer raises a TypeError
since "greater than" is now defined. It outputs False
since both students have the same skill level, so Hermione's skill is not larger than Malfoy's. But you can assign a higher skill to Hermione:
You use the increase_skill()
method to change Hermione’s skill level. This code now outputs True
.
A deeper dive
Let's look at the following statement in a bit more detail:
hermione > malfoy
The first object is hermione
, an instance of Student
. This is the left operand for the >
operator. This operator maps to the special method __gt__()
. When you use this operator, the program calls the method:
hermione.__gt__(malfoy)
The method is called on the first operand, hermione
. The second operand, malfoy
, is passed to the method as an argument. This is equivalent to the following call:
Student.__gt__(hermione, malfoy)
The objects hermione
and malfoy
are passed to the parameters self
and other
, in that order.
Special Methods
You learned about methods in Year 3 at Hogwarts School of Codecraft and Algorithmancy. But the method you define, __gt__()
, is not just any method. It gives meaning to "greater than" for the Wizard
class and any of its subclasses. You can't pick any name for this method as you do for other methods. This is a special method, and its name is fixed.
You already used a method whose name you didn't choose, with a similar naming pattern. This is the __init__()
method, another of the special methods you can define in a class.
All special methods have set names which start and end with double underscores. For this reason, these special methods are also called dunder methods. 'Dunder' stands for double underscores.
Another name sometimes used to refer to these methods is magic methods. You may think this is appropriate, given the theme of this series. However, there's nothing magical about these methods so I don't like using this term.
When Are Wizard
Objects Equal?
Let's look at another special method. You can define __eq__()
to determine when two objects should be considered equal. It's up to you, the programmer, to decide what equality means in the context of the classes you create.
In this example, you'll consider two Wizard
objects to be equal if they have the same name, patronus, and year of birth:
The __eq__()
method returns True
if all three conditions are True
.
You can create two instances that represent the same wizard or witch and check if they're equal:
This code returns True
since the data attributes .name
, .patronus
, and .birth_year
are all the same. The fourth argument that matches .school_year
doesn't have to be the same. You can try changing the fourth argument in one of the instances to see that you still get True
from this code.
is
is not ==
This is a good point for a minor detour. The special method __eq__()
defines equality for two instances, which you can check with the ==
operator. This is not the same as using the is
keyword. You can try using is
in the example with the two versions of Hermione:
The output from this code is the following:
True
False
In this case, the output is different if you use ==
or is
. You've seen how the __eq__()
special method determines what happens when you use the ==
operator. So what does the is
keyword do? It checks whether the objects on either side of the keyword are the same object—not two objects wiith the same value, but the same object.
You can read more about the difference between ==
and is
in the following article: Understanding The Difference Between `is` and `==` in Python: The £5 Note and a Trip to a Coffee Shop.
And what would happen if the __eq__()
special method was missing from the class definition? You can comment the code to find out:
Now that the Wizard
class and any classes that inherit from it no longer have the __eq__()
special method, you can run the code in making_magic.py
again:
Note there's an additional line that prints the result of hermione == hermione
. The output of this code is the following:
False
False
True
The ==
operator no longer returns True
when you compare hermione
with other_hermione
. However, it does return True
when you use it with the same object on either side of the operator.
If the __eq__()
operator is not defined, the ==
operator defaults to returning True
if and only if the objects on either side of the operator are the same object.
Ensure you uncomment the code block with the __eq__()
method before proceeding.
The equality and comparison special methods
You've seen how __gt__()
defines what "greater than" means and ==
what "is equal to" means. There are other comparison operators. Here's the full list:
__lt__()
is "less than"__le__()
is "less than or equal to"__eq__()
is "is equal to"__ne__()
is "not equal to"__gt__()
is "greater than"__ge__()
is "greater than or equal to"
There are a couple of shortcuts if you don't want to define all of these all the time. You can use the total_ordering
decorator in functools
or data classes if appropriate. But that's a topic for after graduation from Hogwarts School of Codecraft and Algorithmancy.
More Special Methods
There are more special methods. Many more. And I will not go through all of them here, thankfully! But let's look at another three.
Let's create a few students and a house in making_magic.py
. You can then assign the students to a house:
You create three instances of the Student
class and one instance of House
. Next, you use the assign_house()
method you defined in the Wizard
class in an earlier article in this series. Since Student
inherits from Wizard
, it also has access to this method.
There are now three students in Gryffindor. So, let's try to loop through the House
object to get all its members:
But this code raises a TypeError
:
Traceback (most recent call last):
... line 35, in <module>
for student in gryffindor:
TypeError: 'House' object is not iterable
The error tells you that the House
object is not iterable. You cannot get one item at a time from it as is needed in a for
loop. To make an object iterable, the class must have another special method: __iter__()
.
Iteration using __iter__()
I'll gloss over the intricacies of using __iter__()
to make a class iterable. I dealt with this topic in another recent post, part of the Data Structure Categories Series: Iterable: Python's Stepping Stones. And while you're there, you can also read another article in that series. This one is about iterators: A One-Way Stream of Data • Iterators in Python.
You need to decide what "iteration" means for your class. In this case, you can choose to iterate over the list that stores the house members:
The __iter__()
special method makes this class iterable. The method must return an iterator. In this case, you return the list iterator created when you pass the list self.members
to the built-in iter()
function.
You made a change to the class definition in hogwarts_magic.py
. You can try running making_magic.py
again without making any changes to it. This is the output:
<hogwarts_magic.Student object at 0x10114ad10>
<hogwarts_magic.Student object at 0x10114ad50>
<hogwarts_magic.Student object at 0x10114add0>
Good news (first). You don't get the TypeError
you got earlier. The House
class is now iterable since you added the __iter__()
special method to it. The for
loop iterates three times.
But not all is well. The output you get is not clear. Who's who? You hope to get information about the students who are in Gryffindor. Instead, you get cryptic outputs. The output shows you that you have three different Student
objects. You know they're different since they're stored in different memory locations, which is what the hex number shows.
This is a problem you can solve with another special method!
String representations using __str__()
and __repr__()
Your class doesn't know how to represent the object when you print it out. By default, it shows you the representation you saw earlier. This is the output when you use Python's most common implementation, CPython.
However, you can choose how the object is displayed. Let's start by defining the __str__()
special method. You can define this in the Wizard
class:
This special method returns a string, which is used in some instances when the object needs to be represented and displayed to the program's user, such as when using print()
. In this example, you use the string stored in the data attribute .name
.
You can run making_magic.py
without making any changes to it. This is the output you get now:
Hermione Granger
Ron Weasley
Harry Potter
That's better. The cryptic output has now been replaced by the students' names. So that's it. Problem solved, right?
But there's always some twist to the plot. Let's make a few more changes.
You defined the __str__()
special method in the Wizard
parent class. Therefore, both Student
and Professor
inherit from it. But you've seen in an earlier article that you can override any method in a child class. Let's make the string representation for the Student
class a bit more informative by defining this special method specifically in the Student
class in addition to the __str__()
method you just defined in Wizard
:
The child class Student
has its own __str__()
method now. Therefore, this is the method that's used when it's needed. This output should show the student's name, house, and the school year they're in. Once again, you can run making_magic.py
without making any changes to it. The output is the following:
Hermione Granger (House: <hogwarts_magic.House object at 0x102daad90> | Year 1)
Ron Weasley (House: <hogwarts_magic.House object at 0x102daad90> | Year 1)
Harry Potter (House: <hogwarts_magic.House object at 0x102daad90> | Year 1)
Now you have printouts with some useful information, the name and school year, and some less useful information, the cryptic output in place of the house information. You can fix this by also adding the __str__()
method to House
:
The output from the unchanged making_magic.py
script is now the following:
Hermione Granger (House: Gryffindor | Year 1)
Ron Weasley (House: Gryffindor | Year 1)
Harry Potter (House: Gryffindor | Year 1)
It seems that __str__()
resolves all the string representation problems you may have. But there's another twist coming soon.
Let's look at one final example. This time, you'll use an interactive REPL/Console session:
Even though there's a __str__()
special method, the REPL's output returns the cryptic output again. This is because there are two different string representations that are relevant in this case. One is designed for the user of a program and is set using __str__()
. This is the string representation used in print()
, for example.
There's also a string representation intended for the programmer that's typically used while developing and debugging code. This is the string representation used when you display an object in an interactive session, such as Python's standard REPL. This representation is determined using another special method, __repr__()
.
You can define this special method for the Wizard
class since this is a parent class for both Student
and Professor
. The representation returned by __repr__()
is often a string that can be used to recreate the object:
Since you made a change to the module hogwarts_magic.py
, you can use a new interactive REPL/Console session to re-run the test from earlier:
This is fine. But there's a problem. You created an instance of Student
, but the output from __repr__()
refers to the parent class, Wizard
. Technically, a student is also a wizard, so this is not wrong. But we can do better.
Instead of hard-coding the class name Wizard
in the __repr__()
method's return
statement, you can dynamically get the class name of the object itself:
You get the data type for self
using the built-in type()
function and then use .__name__
to return this name as a string. The output now shows the child class's name. And you can also test an instance of the Professor
class:
The __repr__()
method shows the correct class name in both instances. However, neither of these string representations can be used to recreate the objects. Student
needs a fourth parameter, school_year
, and Professor
also needs another parameter, subject
.
You can make these child classes compatible with the parent class by assigning default values to the fourth parameters in both classes:
This change makes subject
an optional argument for Professor
, and school_year
is also optional for Student
. You can also define separate __repr__()
special methods for the two child classes if you wish.
You can read more about the two string representations and the special methods __str__()
and __repr__()
in an article I wrote for Real Python a while ago: When Should You Use `.__repr__()` vs `.__str__()` in Python?
You can use the ‘copy code’ link at the bottom of the last code section above to access the full hogwarts_magic.py
file.
Terminology Corner
Special Method: A method in a class that defines specific behaviour linked with built-in Python operations. Special methods have set names that start and end with double underscores. Also known as dunder methods.
The sixth year is over. It wasn't a walk in the park. But then, the students weren't expecting it to be. It’s time for a well-earned rest over the holidays, and then—just one more year left.
Code in this article uses Python 3.11
Stop Stack
#21
Recently published articles on The Python Coding Stack:
A Picture is Worth More Than a Thousand Words • Images & 2D Fourier Transforms in Python. For article #20 on The Python Coding Stack, I'm revisiting one of my tutorials from the pre-Substack era
A One-Way Stream of Data • Iterators in Python (Data Structure Categories #6) Iterators • Part 6 of the Data Structure Categories Series
Collecting Things • Python's Collections (Data Structure Categories #5) Collections • Part 5 of the Data Structure Categories Series
And Now for the Conclusion: The Manor's Oak-Panelled Library and getitem()[Part 2]. The second in this two-part article on Python's
__getitem__()
special methodThe Anatomy of a for Loop. What happens behind the scenes when you run a Python
for
loop? How complex can it be?
Recently published articles on Breaking the Rules, my other substack about narrative technical writing:
Mystery in the Manor (Ep. 4). The best story is the one narrated by the technical detail itself
Frame It (Ep. 3). You can't replace the technical content with a story. But you can frame it within a story.
Whizzing Through Wormholes (Ep. 2). Travelling to the other end of the universe—with the help of analogies
Sharing Cupcakes (Ep. 1). Linking abstract concepts to a narrative • Our brain works in funny ways
Once Upon an Article (Pilot Episode) …because Once Upon a Technical Article didn't sound right. What's this Substack about?
Stats on the Stack
Age: 3 months, 2 weeks, and 1 day old
Number of articles: 21
Subscribers: 774
Each article is the result of years of experience and many hours of work. Hope you enjoy each one and find them useful. If you're in a position to do so, you can support this Substack further with a paid subscription. In addition to supporting this work, you'll get access to the full archive of articles and some paid-only articles.