The Python Coding Stack • by Stephen Gruppetta

Share this post

Let The Real Magic Begin • Creating Classes in Python

thepythoncodingstack.substack.com

Let The Real Magic Begin • Creating Classes in Python

Year 2 at Hogwarts School of Codecraft and Algorithmancy • Defining Classes • Data Attributes

Stephen Gruppetta
May 3, 2023
11
2
Share
Share this post

Let The Real Magic Begin • Creating Classes in Python

thepythoncodingstack.substack.com

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:

class Wizard:     ...

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:

class Wizard:     def __init__(self):         self.name = None         self.patronus = None         self.birth_year = None         self.house = None         self.wand = None
copy code text

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:

class Wizard:     def __init__(self):         self.name = None         self.patronus = None         self.birth_year = None         self.house = None         self.wand = None  # Create two wizard instances harry = Wizard() hermione = Wizard()
copy code text

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:

class Wizard:     def __init__(self):         self.name = None         self.patronus = None         self.birth_year = None         self.house = None         self.wand = None  # Create two wizard instances harry = Wizard() hermione = Wizard()  print(harry) # <__main__.Wizard object at 0x102c55110> print(hermione) # <__main__.Wizard object at 0x102c55650>
copy code text

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:

# ...  print(harry.name) # None print(hermione.name) # None

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:

class Wizard:     def __init__(self):         self.name = None         self.patronus = None         self.birth_year = None         self.house = None         self.wand = None  # Create two wizard instances harry = Wizard() hermione = Wizard()  # We'll do this for now, but we'll learn a better way later harry.name = 'Harry Potter' hermione.name = 'Hermione Granger'  print(harry.name) # Harry Potter print(hermione.name) # Hermione Granger
copy code text

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:

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
copy code text

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:

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  # Create two wizard instances harry = Wizard() hermione = Wizard()
copy code text

This code will raise an error when you run it:

Traceback (most recent call last):   ...     harry = Wizard()             ^^^^^^^^ TypeError: Wizard.__init__() missing 3 required positional     arguments: 'name', 'patronus', and 'birth_year'

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:

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  # Create two wizard instances harry = Wizard("Harry Potter", "stag", 1980) hermione = Wizard("Hermione Granger", "otter", 1979)
copy code text

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 to name

  • The string "stag" is the third argument and is assigned to patronus

  • The integer 1980 is the fourth argument and is assigned to birth_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:

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  # Create two wizard instances harry = Wizard("Harry Potter", "stag", 1980) hermione = Wizard("Hermione Granger", "otter", 1979)  print(harry.name) # Harry Potter print(harry.patronus) # stag print(hermione.name) # Hermione Granger print(hermione.patronus) # otter
copy code text

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!

11
2
Share
Share this post

Let The Real Magic Begin • Creating Classes in Python

thepythoncodingstack.substack.com
Previous
Next
2 Comments
Md. Al-Imran Abir
May 8

Until this article, I knew the name as "Garinger"!

Expand full comment
Reply
1 reply
1 more comment…
Top
New
Community

No posts

Ready for more?

© 2023 Stephen Gruppetta
Privacy ∙ Terms ∙ Collection notice
Start WritingGet the app
Substack is the home for great writing