Casting A Spell • More Interaction Between Classes (Harry Potter OOP Series #4)
Year 4 at Hogwarts School of Codecraft and Algorithmancy • More on Methods and Classes
The Great Hall gleamed in the golden light of floating candles, each flicker illuminating eager faces of fourth-year students. In their hands, they tightly gripped a book that shimmered with the promise of the mysteries within:
Wizards' Wisdom: Crafting Class Interaction and Mastering Methods
Last year's adventure had unfurled the power of methods, transforming these young wizards into skilled Python conjurers. As the whispers of their previous lessons echoed in the ancient hall, they stood ready for a new challenge.
The next stage of their journey has secrets that are ready to be unveiled, forming the key to the unexplored realm of Pythonic interaction. Each spell, each method is a new door leading to an intricate maze of interconnected classes.
Fourth-years, as the Great Hall shines on your eager faces, brace yourselves for a new expedition into the depths of Python's magical world. A grand saga of learning, discovery, and mastery awaits you.
In the fourth year at Hogwarts School of Codecraft and Algorithmancy, students learn more about methods and about interaction between different classes.
The Curriculum at Hogwarts (Series Overview)
This is the fourth 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 (this article): 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
Wizard, Wand, House… Spell
One of the key points about object-oriented programming we discussed in earlier years is that we should think of the problem from the point of view of the "objects" that make sense to the person describing the problem.
So far, you have created three classes to describe the wizarding world:
Wizard
House
Wand
Here are the classes as you left them at the end of Year 3:
Unless your memory is too hazy after the long holidays, you'll recall that you added the Wand
class towards the end of Year 3. And it doesn't have much in it, for now. We'll return to the Wand
class soon.
But first, let's add a new class. Wizards have wands. And what do they do with those wands? They cast spells, of course:
This class has three data attributes and one method. The data attributes include two strings with the name of the spell and what effect it has. The third data attribute is a float between 0
and 1
, indicating the spell's difficulty.
The method is_successful()
returns a Boolean to indicate whether the spell worked.
Now, you can get back to the Wand
class and the cast_spell()
method you didn't finish in Year 3:
The argument you pass to Wand.cast_spell()
is a Spell
object. The method will either return the spell's effect or None
.
Wizards Cast Spells
However, the wand can't cast a spell by itself. It needs a wizard. Let's add a cast_spell()
method to the Wizard
class, too:
You can call this method anything you like. But you can also call it cast_spell()
, which is the same method name as the method in the Wand
class. Wand.cast_spell()
and Wizard.cast_spell()
are different methods. They belong to different classes, even though they share the same name.
In this article, I've given them the same name to stress the point that methods of different classes can have the same name. There is an argument for using different names to make the code more readable and minimise bugs. But I won't make that argument here, and I'll carry on with both methods sharing a name. So beware which of the two cast_spell()
methods you're using in different parts of your code.
You made several changes to the classes in hogwarts_magic.py
. You can now check these changes work by trying them out in making_magic.py
, the script you've been using to test these classes:
You create several objects in this script. There are two Wizard
objects and a House
object. You don't need the house in this script, but this was an object you explored in previous years, so you can leave it there to use later.
Next, you create a list of spells. All the items in the list are Spell
objects. You also create a Wand
object which you assign directly to harry
.
When you call harry.cast_spell()
with one of the spells as an argument, Harry Potter will either cast a spell successfully or not. Initially, you call this method twice with the first two spells in the list as arguments. Both are low-difficulty spells (0.3
and 0.1
), and Harry casts them successfully. There is a chance you get a different outcome since the spell's success is random.
When you call hermione.cast_spell()
, you get a warning telling you that she doesn't have a wand. The final else
clause in Wizard.cast_spell()
is responsible for printing this message.
Finally, you use a for
loop to get Harry to attempt to cast the same spell ten times. This is the third spell in the list and it's a difficult one to cast (0.8
difficulty). Harry managed to cast it successfully only four times in this case. That's slightly more than expected on average for a spell with difficulty 0.8.
But wait a minute. Shouldn't the skill of the wizard matter too? Surely, Dumbledore has a much higher success rate than Crabbe! The likelihood of a spell succeeding depends on the spell's difficulty and the wizard's abilities. Let's add a new data attribute to the Wizard
class:
Passing information between objects of different classes
You want to consider the wizard's skill when he or she casts a spell. So you need to make some changes. You could pass the wizard's .skill
data attribute to Wand.cast_spell()
. But you can also pass the entire Wizard
object. Let's use the latter option.
First, you need to update Wizard.cast_spell()
. You pass the Wizard
object to the wand's cast_spell()
method. Recall that we have two methods in different classes with the same name:
The only addition is the second argument in self.wand.cast_spell(spell, self)
. There's a lot happening in this line, so let's break it down, from left to right. Recall that this function is in the Wizard
class:
self.wand.cast_spell(spell, self)
self
The nameself
refers to theWizard
object itself. Therefore, if you call this method onharry
, thenself
refers to the same object that has the nameharry
self.wand
One of the data attributes of theWizard
class is.wand
, which refers to an object of typeWand
if a wand has been assigned to the wizardself.wand.cast_spell
Whereaswand
is an attribute ofself
,cast_spell
is an attribute ofwand
. TheWizard
object,self
, has an attributewand
, andwand
has an attributecast_spell
, which is a method. Therefore, this will callWand.cast_spell()
self.wand.cast_spell(spell, self)
You pass two arguments to.wand.cast_spell()
. The first is an object of typeSpell
. The second argument isself
, which is a reference to theWizard
object
You'll also need to update the definition for Wand.cast_spell()
, but you'll do that later. First, let's look at an example using harry
as the Wizard
object. Let's assume there is a Wand
named a_wand
and a Spell
named a_spell
. The method call is:
harry.cast_spell(a_spell)
You'll recall from Year 3 that this method call is equivalent to:
Wizard.cast_spell(harry, a_spell)
The object is passed to the method as its first argument. Within Wizard.cast_spell()
, you can focus on this line:
effect = self.wand.cast_spell(spell, self)
I'll translate this using the object names harry
, a_wand
, and a_spell
to help understand what's going on. You don't need to write this anywhere in your code since this is roughly equivalent to what happens "behind the scenes":
effect = harry.a_wand.cast_spell(a_spell, harry)
Before I move on, let me try to represent these steps through a set of diagrams. There are three objects. I'll colour-code anything relating to the Wizard
class in orange. Purple is for the Wand
object and green for the Spell
object:
The whole process is triggered by the call harry.cast_spell()
:
And then, as we did earlier, we'll focus on the expression self.wand.cast_spell(spell, self)
:
You can see how we move from orange to purple. The Wizard
data attribute .wand
is shown in orange since it's part of the Wizard
class. This attribute is the Wand
object a_wand
, which is shown in purple.
Finally, you call the .cast_spell()
method of the Wand
class, shown in purple, and you pass the Spell
object a_spell
and the Wizard
object harry
to it.
Update Wand.cast_spell()
In the change you made in the previous section, you passed two arguments to Wand.cast_spell()
. Therefore, you need to update this method. Let's focus our attention back on the Wand
class. While you're here, you can also add a new data attribute to the class to allow wands to have different powers:
There's a new .power
data attribute. And cast_spell()
now has an additional parameter, wizard
. There's one more change in this code. You now pass two arguments to spell.is_successful()
. The first argument is self
. Since this is a method within the Wand
class, self
refers to the Wand
instance. The second argument is the reference to the Wizard
instance.
But this means that Spell.is_successful()
also needs to be updated:
You can come up with your own formula to determine whether a spell is successful. In this example, the probability from the spell's difficulty alone is multiplied by the wand's power and the wizard's skill, both of which are floats in the range 0
to 1
.
End-of-Year Feast
It's time to start wrapping up this year at Hogwarts School of Codecraft and Algorithmancy. Here are the classes as they stand at the end of Year 4 (Note that you can get access to the full code, without any of the gaps, for all code snippets by following the copy code link in the caption):
You can extend these classes with more methods. Here are some ideas:
Create a
.spells_known
attribute in theWizard
class. This is a list of known spellsAdd
learn_spell()
with adds spells to.spells_known
When a wizard casts a spell, you need to check that he or she knows that spell first!
You can use your imagination and think of other ways to extend these classes.
It's important to note that this example is a demonstration example. We're not necessarily trying to write classes that are useful in a real-life situation. My aim in this series is to write a set of classes that go beyond the basic ones you often find in introductory tutorials on object-oriented programming but without writing excessively long and complex code.
One of the key points of OOP, and one that we reinforced in Year 4, is that we want to "encapsulate" the data and functionality into one unit—the object. And a class is the blueprint to create those objects.
Once a class's data attributes and methods are defined, a user of the class can deal with these objects at a "higher level". For example, harry.cast_spell()
means you want Harry to cast a spell. You're no longer concerned about what's happening "behind the scenes". The class definitions take care of all the details.
When writing code using OOP principles, you put all the hard work up-front to write the classes. Once these are done, writing programs that can use these classes can be easier.
Time to pack your bags, say goodbye to your classmates and head back home on the Hogwarts Express. Make sure you rest during the holidays, as Year 5 will be another busy year. You'll learn about inheritance.
Next article in this series: Year 5 • "You Have Your Mother's Eyes" • Inheritance in Python Classes
Code in this article uses Python 3.11
Stop Stack
Recently published articles on The Stack:
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?
There's A Method To The Madness • Defining Methods In Python Classes. Year 3 at Hogwarts School of Codecraft and Algorithmancy • Defining Methods
Code snippets in the articles: I've been exploring the best ways to display code until Substack implements a code block that is useable on their platform. Currently, I am:
Putting a code snippet image with the code. For long programs, such as in this article, I only show the sections that have changed and include # ... as placeholders for the rest of the unchanged code
I include the whole code in the ALT text, although substack doesn't make it easy to access this text to copy and past
I include a "copy code" caption with a link to a Gist on Github which includes the full code, no truncations
When Substack introduces better ways of dealing with code in articles, I'll shift to using their native code blocks, but in the meantime I think this provides a good solution. The code snippet images make it easy to read the code as it's properly formatted and clear. The link to the Gist allows you to copy the code.
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!
Appendix: Code Blocks
Code Block #1
# Auto-generated from an interactive REPL/Console session
# # hogwarts_magic.py
# 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
# 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)
#
# 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):
# self.wood = wood
# self.core = core
# self.length = length
# def cast_spell(self, spell):
# # We'll get to this later, in Year 4
# ...
Code Block #2
# Auto-generated from an interactive REPL/Console session
# # hogwarts_magic.py
# import random
# # ...
# 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):
# success_rate = 1 - self.difficulty
# return random.random() < success_rate
Code Block #3
# Auto-generated from an interactive REPL/Console session
# # hogwarts_magic.py
# # ...
# class Wand:
# def __init__(self, wood, core, length):
# self.wood = wood
# self.core = core
# self.length = length
# def cast_spell(self, spell):
# if spell.is_successful():
# return spell.effect
# return None # Explicitly return None (for readability)
# # ...
Code Block #4
# Auto-generated from an interactive REPL/Console session
# # hogwarts_magic.py
# import random
# class Wizard:
# # ...
# def cast_spell(self, spell):
# if self.wand:
# effect = self.wand.cast_spell(spell)
# 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!")
# # ...
Code Block #5
# Auto-generated from an interactive REPL/Console session
# # making_magic.py
# from hogwarts_magic import Wizard, House, Wand, Spell
# harry = Wizard("Harry Potter", "stag", 1980)
# hermione = Wizard("Hermione Granger", "otter", 1979)
# gryffindor = House(
# "Gryffindor",
# "Godric Gryffindor",
# ["scarlet", "gold"],
# "lion",
# )
# spells = [
# Spell("Expelliarmus", "Disarming Charm", 0.3),
# Spell("Lumos", "Light Charm", 0.1),
# Spell("Expecto Patronum", "Patronus Charm", 0.8),
# Spell("Alohomora", "Unlocking Charm", 0.2),
# Spell("Wingardium Leviosa", "Levitation Charm", 0.4),
# Spell("Riddikulus", "Riddikulus Charm", 0.5),
# Spell("Stupefy", "Stunning Spell", 0.2),
# Spell("Finite Incantatem", "Finite Incantatem Spell", 0.1),
# Spell("Protego", "Protego Spell", 0.3),
# Spell("Reducto", "Reducto Spell", 0.5),
# ]
# harry.assign_wand(Wand("holly", "phoenix feather", 11))
# harry.cast_spell(spells[0])
# # Harry Potter cast Disarming Charm!
# harry.cast_spell(spells[1])
# # Harry Potter cast Light Charm!
# hermione.cast_spell(spells[2])
# # Hermione Granger has no wand!
# for spell_cast in range(10):
# harry.cast_spell(spells[2])
# # Harry Potter cast Patronus Charm!
# # Harry Potter cast Patronus Charm!
# # Harry Potter failed to cast Expecto Patronum!
# # Harry Potter failed to cast Expecto Patronum!
# # Harry Potter failed to cast Expecto Patronum!
# # Harry Potter cast Patronus Charm!
# # Harry Potter failed to cast Expecto Patronum!
# # Harry Potter failed to cast Expecto Patronum!
# # Harry Potter failed to cast Expecto Patronum!
# # Harry Potter cast Patronus Charm!
Code Block #6
# Auto-generated from an interactive REPL/Console session
# # hogwarts_magic.py
# import random
# class Wizard:
# def __init__(self, name, patronus, birth_year):
# # ...
# 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
# # ...
Code Block #7
# Auto-generated from an interactive REPL/Console session
# # hogwarts_magic.py
# import random
# class Wizard:
# # ...
# 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!")
# # ...
Code Block #8
# Auto-generated from an interactive REPL/Console session
# self.wand.cast_spell(spell, self)
Code Block #9
# Auto-generated from an interactive REPL/Console session
# harry.cast_spell(a_spell)
Code Block #10
# Auto-generated from an interactive REPL/Console session
# Wizard.cast_spell(harry, a_spell)
Code Block #11
# Auto-generated from an interactive REPL/Console session
# effect = self.wand.cast_spell(spell, self)
Code Block #12
# Auto-generated from an interactive REPL/Console session
# effect = harry.a_wand.cast_spell(a_spell, harry)
Code Block #13
# Auto-generated from an interactive REPL/Console session
# # hogwarts_magic.py
# 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)
Code Block #14
# Auto-generated from an interactive REPL/Console session
# # hogwarts_magic.py
# # ...
# class Spell:
# # ...
# def is_successful(self, wand, wizard):
# success_rate = (1 - self.difficulty) * wand.power * wizard.skill
# return random.random() < success_rate
Code Block #15
# Auto-generated from an interactive REPL/Console session
# # 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
Not sure what might have happened, but the "copy code" links are no longer visible.