There's A Method To The Madness • Defining Methods In Python Classes (Harry Potter OOP Series #3)
Year 3 at Hogwarts School of Codecraft and Algorithmancy • Defining Methods
The Hogwarts Express thundered through the landscape, the rhythm of its powerful engines setting the tempo for the excited conversations within. The third-year compartment hummed with a different kind of energy - an air of contemplative excitement. Each held a thick tome, its platinum lettering shimmering with subtle magic:
Secrets of Sorcery: Defining and Deploying Methods
Year 2 had been a fascinating journey through the foundational elements of object-oriented programming in Python, mastering classes and data attributes. But as the train sped towards a new term, the true spellbinding journey was about to begin. With the turning of each page, the third-years would not only become observers of the magic within Python, but also the wizards behind the curtain, creating and controlling their own spells in the form of methods. The Hogwarts Express was not just ferrying them to school, it was hurtling them towards an exciting new chapter of their magical education.
In the third year at Hogwarts School of Codecraft and Algorithmancy, students build on their knowledge of defining classes, and they are now ready to write methods.
The Curriculum at Hogwarts (Series Overview)
This is the third 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 (this article): 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: 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
We Need A Method
The students needed a bit of revision in their first week back at school. This is where they left off at the end of Year 2:
We defined a Wizard
class. It has an __init__()
special method, which is called when a new instance is created. Each Wizard
object has five attributes. These are data attributes—variables "attached" to the object.
The values for three of these data attributes are passed as arguments when you create the object. The other two, .house
and .wand
are empty. Well, they're not technically empty. The None
object is assigned to these.
Data attributes aren't the only attributes. An object is more useful when it also has built-in functionality, not just stored data. We use functions to get things done in a program, and we can add functions within a class, too. We call these methods.
Every Wizard Needs A Wand
You can separate your code into two files. Let's start with hogwarts_magic.py
, which has the Wizard
class. And you can add another method called assign_wand()
:
A method is a function, and we define it similarly by using the def
keyword. This is not the first method defined in Wizard
, since __init__()
is also a method. However, assign_wand()
is not a special method. We'll get back to special methods with double underscores in their names in a later year.
The method assign_wand()
has two parameters:
self
wand
The students at Hogwarts School of Codecraft and Algorithmancy learnt all about self
in their second year. But if you ask them, they'll tell you they're not that confident with self
.
The parameter self
is a placeholder we use in the class definition to refer to the object we'll create later. Both __init__()
and assign_wand()
have self
as the first parameter. If you're using an IDE to code, it's likely the parameter self
was automatically filled in since most methods will have it.
These are instance methods. Every instance of the class will have access to these methods. We'll look at why their first parameter is self
shortly.
The assign_wand()
method performs two actions:
It assigns the value of the argument
wand
to the object's data attributeself.wand
. This way, the object carries this information with itIt prints out a message to show the wand assigned to the wizard
Let's test this method in a separate file called making_magic.py
. The output from the code is shown as comments in this code segment:
Since you use a different script to the one where you define the Wizard
class, you start by importing the class from hogwarts_magic.py
.
Initially, the .wand
data attributes for both objects contain None
. The wizards each get a wand when you call .assign_wand()
on the objects. harry.assign_wand()
only affects harry
and hermione.assign_wand()
only affects hermione
. The wands are assigned to the .wand
attribute for each object.
This is a key feature of object-oriented programming. Everything is self-contained within the objects.
Another Look At self
The definition of assign_wand()
within the class definition has two parameters, self
and wand
. However, when you call the method in making_magic.py
, you only include one argument.
When you call an instance method, such as .assign_wand()
, the object itself is automatically passed as its first argument and assigned to the self
parameter. Therefore, the call:
harry.assign_wand(
"holly, phoenix feather, 11 inches"
)
is equivalent to:
Wizard.assign_wand(
harry,
"holly, phoenix feather, 11 inches"
)
An instance method always has access to the instance and all its other attributes.
A minor note: The parameter name self
is not a special word. You can name it anything you want. The instance itself is always passed to the first parameter in an instance method. However, the convention to name the first parameter of an instance method self
is a very strong one. So, you should stick with calling it self
.
Every Wizard Needs A House
We discussed the OOP mindset in Year 1. What are the 'things' that matter for a human looking at this problem? What 'things' or 'objects' are needed to understand the problem?
We've already identified 'wizard' as a 'thing' that's relevant, and we defined a class called Wizard
. Houses and wands are also 'things' needed to represent the wizarding world. So, we'll create classes for each one of them.
Let's start with House
. Its data attributes could include the following:
.name
.founder
.colours
.animal
.members
.points
You can start defining the class and its __init__()
method:
The first four data attributes are matched to the __init__()
method parameters that come after self
. Therefore, you'll need to pass these values as arguments when you create an instance of House
.
self.members
and self.points
are initialised to an empty list and the integer 0
. These are the values every House
instance will have for these data attributes when the instance is created.
Now, you can add some methods to the class House
:
Three of these methods update a data attribute. They're changing the state of the object. The final method doesn't make any changes to the object. Instead, it returns some information. In this case, it returns a dictionary with relevant values.
Note that add_member()
, remove_member()
, and update_points()
do not have an explicit return
statement. These methods don't need to return any data. Their purpose is to change values in the object's data attributes. Like all functions without an explicit return
statement, they return None
.
Let's look at the Wizard
class again. You'll recall that we assigned a string with the name of the wand to the .wand
data attribute:
You could do the same thing with the Wizard
object's .house
data attribute and assign a string with the house name. But now we have a House
class, and therefore, objects of type House
. So, we can assign a House
instance to self.house
in the Wizard
class. You can add an assign_house()
method to the Wizard
class:
Let's look at Wizard.assign_house()
. There are two actions in this method:
The value passed to the method is assigned to the data attribute
self.house
. This value will be an instance of typeHouse
. (You could use type hinting and input validation, too)The
House
instance itself is modified. The argument passed tohouse
is an instance ofHouse
. Therefore,house.add_member()
calls theHouse
instance's.add_member()
method. You passself
to this method—that's the wizard!
As we discussed earlier, self
is a placeholder, which refers to the instance of a class. When you use self
within the Wizard
class, it refers to a Wizard
instance. And when you use self
within the House
class, you got it, it refers to a House
instance.
Assigning houses to wizards
Let's test these new additions by updating making_magic.py
:
You create wizards and a house. Initially, harry.house
is None
and gryffindor.members
is an empty list. The wizard has no house, and the house has no members.
Once you call harry.assign_house(gryffindor)
, both associations are in place. You confirm this by printing harry.house.name
and by looping through gryffindor.members
and printing the .name
attribute for the items in the list. For now, I'm not printing harry.house
or gryffindor.members
directly. You can try this out in your code to see what happens. We'll return to this issue in a later year!
Let's test this a bit more. What happens if we also add gryffindor.add_member(harry)
in the code?
Harry Potter appears twice in the list of house members. You added him when you called harry.assign_house(gryffindor)
and again when you called gryffindor.add_member(harry)
.
Note that you'll see "Harry Potter"
printed three times when you run the script. The first is the printout from the first for
loop, and the remaining two are the printouts from the second for
loop, the one after the gryffindor.add_member(harry)
call.
We can make some changes to House.add_member()
:
This will ensure a wizard will only be added once to a house.
Revisiting Wands
At the start of this year, we needed to assign wands to wizards. We assigned a string with the wand's details. We can do better. We can define a Wand
class:
The Wand
class has three data attributes with the wand's characteristics. There's also a cast_spell()
method, which we'll work on next year. And to test this new class, we can update making_magic.py
:
You import Wand
along with the other classes. You create an instance of Wand
in the call to harry.assign_wand()
. Therefore, the only reference to this Wand
object is through the object harry
.
The line printed out when you call harry.assign_wand()
doesn't look right. I hinted at this problem earlier in this article, and we'll resolve this in a later year. So, for now, let's ignore this weird output.
When writing classes, you put all the hard work into creating the classes and ensuring the associations and behaviours are all right. But then, using the classes is safer and easier.
We'll build on these classes in Year 4.
Terminology Corner
Instance method: A function that's part of a class (a method), which is attached to each instance of the class. An instance method takes the object as its first argument. This is implicit when you call the method—you don't need to add it as an argument.
This was a busy year. You've earned your holidays. Year 4 at Hogwarts School of Codecraft and Algorithmancy will focus on the interaction between objects of different classes.
Next article in this series: Year 4 • Casting A Spell • More Interaction Between Classes
Code in this article uses Python 3.11
Stop Stack
Recently published articles on The Stack:
Finding Your Way To The Right Value • Python's Mappings. Part 3 of the Data Structure Categories Series
Python's functools.partial() Lets You Pre-Fill A Function. An exploration of partial() and partialmethod() in functools
Let The Real Magic Begin • Creating Classes in Python. Year 2 at Hogwarts School of Codecraft and Algorithmancy • Defining Classes • Data Attributes
Sequences in Python. Sequences are different from iterables • Part 2 of the Data Structure Categories Series
Harry Potter and The Object-Oriented Programming Paradigm. Year 1 at Hogwarts School of Codecraft and Algorithmancy • The Mindset
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. Thank you!