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 2 (this article): Students will start defining classes and learning about data attributes
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
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
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!
__init__() method creates five data attributes:
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
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.
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,
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.
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
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
How To Refer To The Object Itself (Understanding
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:
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
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,
hermione, you can use the variable names:
But, in the class definition, you write:
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
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:
TypeError complains about the three missing arguments that must be assigned to the parameters
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
__init__(). In this case, this is the same object you're naming
__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
Let's change the code that creates the two
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
"Harry Potter"is the second argument and is assigned to
"stag"is the third argument and is assigned to
1980is the fourth argument and is assigned to
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
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
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
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!