Let The Real Magic Begin • Creating Classes in Python (Harry Potter OOP Series #2)
Year 2 at Hogwarts School of Codecraft and Algorithmancy • Defining Classes • Data Attributes
A palpable excitement knitted the cool breeze as students, reunited after their long parting at the end of the previous year, exchanged eager smiles and shared stories. The largest congregation gravitated towards the bustling bookshop, where scholars of all ages sought out their newest tomes. Among them, the second-year students stood out with a distinctive twinkle in their eyes, clutching the thick volume, its gold lettering shimmering magically at periodic intervals:
Wonders of Wizardry: Conjuring Classes and Data Attributes from Thin Air
Year 1 had been a thrilling introduction, but the real enchantment was about to commence with the challenges of Year 2.
The second year at Hogwarts School of Codecraft and Algorithmancy moves from the philosophical outlooks of Year 1 to the practical aspects of object-oriented programming. But ensure you keep looking at things with the OOP mindset you learnt in Year 1.
The Curriculum at Hogwarts (Series Overview)
This is the second 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 (this article): 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: 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
The Making of a Wizard (Class)
We want to create code to deal with everything that goes on at Hogwarts School of Codecraft and Algorithmancy. We have to deal with the interactions between students, professors, and the subjects. Every student is part of a house, so we'll also deal with houses. And all wizards have wands, which they use to cast spells.
You don't need object-oriented programming to do this. If you revised your Year 1 material well, you'd know that OOP is a programming paradigm that you can choose to use if you want. If you're not using OOP, you could create nested data structures to store information about each wizard and functions to assign a wand to a wizard, for example. You could also have a function to assign a wizard to a house. But you also need to update the data structure that represents the house!
Object-oriented programming offers an alternative approach. We will no longer need standalone data structures and functions. Instead, we'll bundle these into classes and the objects we create from these classes.
Heads up! If you're new to defining classes in Python, beware there's a lot of new syntax involved. There are also some new terms. Don't let this put you off. I won't try to explain everything all at once, but I will explain the new syntax and terminology throughout the article and the series.
Let's start by creating our first class:
You use the class
keyword followed by the name you choose for your class. By convention, we use an uppercase first letter for class names consisting of a single word and UpperCamelCase
for names with multiple words.
Once we start writing it, the block of code that follows the colon will be the template or blueprint needed to create lots of wizards.
What Happens When You Create a Wizard
Object?
I promised you some new syntax and terminology! So let's start with the term method. A method is a function that belongs inside a class. Therefore, any function you define within a class definition is a method.
Since all methods are functions, everything you've ever learnt about functions also applies to methods.
Let's start by defining a method. Often, the first thing you create when you define a class is the __init__()
method. This method defines how a new instance of this class is initialised (which is where the method name __init__
comes from).
An instance is an object created from a class. You may wonder whether there's a difference between the terms 'object' and 'instance'? The answer is "not really", except for the context in which they're used. When you want to make a specific reference to an object that's created from a class, you can use 'instance'. However, every object belongs to a class. Therefore, every object is an instance. In brief: you can use the terms interchangeably!
What information should this new object have? Is there anything else needed to set it up? The __init__()
method contains answers to these questions. Let's start with a basic version:
There's lots of mystery at Hogwarts School of Codecraft and Algorithmancy. Of course there is! The name self
is one of them. I'd like you to ignore self
for a few more paragraphs, but I promise I won't ignore it for long!
The __init__()
method creates five data attributes: name
, patronus
, birth_year
, house
, and wand
. If you're not au fait with the Harry Potter stories and are wondering what on Earth a 'patronus' is, you don't need to worry—it's irrelevant to our story.
A data attribute is a reference to some data and is attached to an instance of a class. This means that every instance of the Wizard
class will have its own name
, patronus
, birth_year
, house
, and wand
attributes.
Data attributes are variable names. They are sometimes also called instance variables instead of data attributes. Both terms are insightful, and it's useful to know both. They are variables that belong to an instance, and they are attributes that contain data! However, Python prefers the term data attributes. The term variable is used less often in Python than in other languages since we often use the term name to refer to objects.
You can see why things can seem confusing with all these terms! Some have the same meaning. Others only have subtle differences from terms we already know.
Creating a Wizard
object
When you define a class, you create a template or blueprint to make objects that are similar to each other. So, let's create instances of the class:
You create two objects by writing the class name followed by parentheses. You assign these objects to two variable names, harry
and hermione
. Note that you create these objects in the main program and not within the class definition—there is no indent! In later years, we'll separate our code into different files, but this will do fine for now.
Both objects are of the same type, Wizard
, but they're not the same object. We can see this even with the limited amount of code we've written so far:
The output you get when you print the two Wizard
objects is a bit cryptic. You can see that both are Wizard
objects (and that Wizard
is defined in the main program you're running.) You can also see the memory location where these objects are stored. The hexadecimal numbers shown are different. This confirms they're not the same object. Similar, yes, but not the same thing!
However, this code still has limited use. For example, if we try to show the name of each wizard, we get None
for both:
When defining the __init__()
method, we've assigned None
to all the data attributes. Let's fix this in the next section.
Special methods
But before we modify __init__()
, let's talk about another term. I defined a method as any function that's within a class. However, the method we have defined so far is not just any method. We cannot choose the name of this method, and it has two underscores at the beginning and another two at the end of its name.
We'll see more of these types of methods with double underscores in their names. They're called special methods, and they are, how shall I say this, methods that are special! They provide specific functionality to the object. In the case of __init__()
, it defines what happens when an object is initialised just after it's created. Although the formal name for these methods is special methods, they're often referred to as dunder methods because of the double underscores at the start and end of the method names.
Another term that's occasionally used for these methods is magic methods. This may seem appropriate given the theme of this series! However, in the "real world", there's nothing magical about these methods, so it's not a term I like to use. In this series, I'll refer to these methods as special methods or dunder methods.
Not All Wizards Are The Same
The data attributes are all set to None
in the current version of the code. You could change them by assigning data to each object's data attribute. I'll show you this option in the code below, but there's a better way of doing this, which we'll explore soon:
Data attributes are variable names attached to objects. Therefore, as we did above, you can treat them like any other variable names and reassign data to them. Both objects have a data attribute called .name
, but they contain a different value.
I often describe a variable as a box with information inside and a label with the variable name on the box's exterior. A data attribute is a box that the object carries along with it wherever it goes! Each object has its own .name
box.
Objects of the same class are similar—all Wizard
objects have a .name
data attribute, for example—but they're not the same. The string you store in harry.name
is different to that in hermione.name
.
How To Refer To The Object Itself (Understanding self
)
Let's make an improvement to the __init__()
method by adding more parameters and using them when defining the data attributes:
A method is a function. Therefore, we can add parameters in the parentheses when we define the method. We already had a parameter in the brackets from earlier—that's self
, which I had asked you to ignore. We've added three more parameters: name
, patronus
, and birth_year
.
But let's ignore self
no more! We can't avoid it forever.
The class definition is a blueprint for creating objects. You'll need a name to refer to those objects when you create them, such as the variable names harry
and hermione
. However, in the blueprint, you still don't have those names. Inside the class definition, you cannot refer to the object by its future name since you haven't created the object yet! The object is created when the program executes the line with Wizard()
on it and not when it encounters the class definition.
self
is the placeholder name we use by convention within the class definition to refer to a future instance of the object. self
refers to the object you will create at some future point. For example, when you create the two instances, harry
and hermione
, you can use the variable names:
harry.name
hermione.name
But, in the class definition, you write:
self.name
It's the placeholder name you put in the class definition to refer to the object.
When you define __init__()
, the first parameter is self
. This means the object is passed to the function whenever you call __init__()
. You'll see that many methods within a class follow this pattern, and we'll talk about this in more detail in Year 3 at Hogwarts School of Codecraft and Algorithmancy.
Let's get back to the __init__()
method we defined at the start of this section. We've seen that self
is the first parameter. The remaining parameters are used to assign data to some of the data attributes we have created. The value passed to the parameter name
when you call __init__()
is assigned to the data attribute self.name
. The same happens to data passed to the other parameters. The parameter names and the data attribute names don't have to be the same name. However, they often are!
There's one last question we need to answer before the end of Year 2:
When and how do we call __init__()
?
Creating a Wizard
object (Part 2)
We've seen that a class is callable. This means you can write Wizard()
to create an instance of the class. When you do so, the class's __init__()
method is called behind the scenes. The object itself is passed as the first argument to __init__()
. We'll return to this in a few paragraphs' time.
Let's update our code to put the Wizard()
calls back in:
This code will raise an error when you run it:
The TypeError
complains about the three missing arguments that must be assigned to the parameters name
, patronus
, and birth_year
. Note how the error refers to Wizard.__init__()
. It also doesn't complain about self
being missing. Let's see what's happening.
When you create an instance of the class by writing harry = Wizard()
, the __init__()
method is called behind the scenes, Wizard.__init__()
, and the newly created object is passed as its first and only argument. This is assigned to self
within __init__()
. In this case, this is the same object you're naming harry
.
However, the __init__()
method defines four parameters. The object is automatically passed to it as its first argument. But the remaining three arguments are missing. This is why the error message mentions name
, patronus
, and birth_year
.
Let's change the code that creates the two Wizard
instances:
Let's look at the line in which you define the variable name harry
. You create an instance of the class by writing Wizard()
with three arguments. This is equivalent to calling Wizard.__init__()
with four arguments:
The newly created object is the first argument and is assigned to
self
The string
"Harry Potter"
is the second argument and is assigned toname
The string
"stag"
is the third argument and is assigned topatronus
The integer
1980
is the fourth argument and is assigned tobirth_year
Four arguments are passed to __init__()
even though there are only three when you create the instance. The newly created object is passed automatically to __init__()
. Remember that the parameter self
in the __init__()
method refers to the object you're creating. In this case, you're labelling this object harry
in the main program.
We'll see this pattern repeated with many methods, not just special methods. We'll talk about methods in Year 3.
Let's check that the two objects have different values for their data attributes:
Both objects have a .name
and a .patronus
data attribute, but their values are different. You can also check that this is the case with .birth_year
.
Terminology Corner
Method: A function that exists within a class
Special Method: One of several methods which control certain features of an object. These methods have pre-established names which start and end with a double underscore, such as
__init__()
. They're often also referred to as dunder methods
Instance: An object created from a class
self
: The placeholder name we use by convention to refer to the object itself within a class definition
It's the last day of term, and the holidays are about to start. This year, you learnt how to define a class and create data attributes. You've defined one of the special methods, __init__()
, which initialises the instance of the object when you create it. And now, you know all about self
!
However, objects of a class don't just contain data. In Year 1, one of the teachers wrote on the board that programming is "storing data and doing stuff with the data". In OOP, the "storing data" and "doing stuff with the data" are bundled into the object. In Year 3, you'll learn how to do stuff with data by defining methods.
Next article in this series: Year 3 • There's A Method To The Madness • Defining Methods In Python Classes
Code in this article uses Python 3.11
Stop Stack
Recently published articles on The Stack:
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
Iterable: Python's Stepping Stones. What makes an iterable iterable? Part 1 of the Data Structure Categories Series
Why Do 5 + "5" and "5" + 5 Give Different Errors in Python? • Do You Know The Whole Story? If
__radd__()
is not part of your answer, read on…
There is now a paid subscription available for The Python Coding Stack. Most articles will remain available 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!
Until this article, I knew the name as "Garinger"!