Pygame Guide for Sneks


Home | GitHub | Pygame Docs


Project maintained by pygame-guide-for-sneks Hosted on GitHub Pages — Theme by mattgraham

Classes in Python and Pygame


Table of Contents

What Are Classes and Why Do We Care?

Python is an object oriented programming language, which means that everything in Python is an object. But that doesn't tell us what objects are. The word "object" here just means a collection of data and/or functionality. More specifically, any piece of data attached to a class is called an "attribute". Attributes can be either variables or functions, but attributes that are functions have another common name: methods. Variable attributes can be called "instance variables" to distinguish them from methods, but when talking about classes with other Python programmers, it is generally accepted that "attribute" usually means a variable and "method" means a function. Now, why do they matter and how do they relate to classes? We'll get to that.

You should be familiar with the built-in datatypes such as:

  • int
  • float
  • str
  • list
  • tuple
  • dict
Each of these built-in datatypes is actually a class! When talking about objects and classes, there are 4 key terms that should be kept in mind. Those terms are "class", "type", "instance", and "object". So let's discuss what these terms mean.

A "class" is ultimately just a container for some data and functionality. The key part to recognize about a "class" is that the "class" term usually specifically refers to the abstract container itself. So int is a class but 1 is not. 1 is what's called an instance of the int class. Note, however, int behaves a bit differently than most classes you'll define for your own use. But, you can explicitly instantiate int in a similar fashion to the way you'll be instantiating your own classes. Just append () to the end of the class name to create an instance (so like int() would return an int instance. You can also put any number into the parentheses and it will create an instance based on that number, otherwise it defaults to 0).

A "type" is pretty much another word for "class" in Python. The type function returns the class that the argument is an instance of (if you print type(1) in Python, you'll get int). But wait, why did I say "pretty much another word for 'class'" and not "another word for class?" Well, that part is complicated and well beyond the scope of this guide. There is a link at the bottom of the page that goes into depth about the relationship between the two concepts in Python. I only mention that there is a difference for the pedants that might take issue with my simplification. So, you can ignore all but the first couple of sentences in this paragraph in 99.99999% of cases.

An "object" is an instance of a class. 1 is an object. Here's how to think about class vs instance: A class is a template for an instance. The class tells you how to construct whatever data structure you're trying to construct, and an instance is what you get when you follow the template. Again for the pedants: technically everything in python is an "object" too, but I'm still simplifying here.

Creating a Simple Class

Now's the time that we can start actually making classes. So, let's start off by creating a very simple one. Here's a class definition that we will parse:

1
2
3
class Foo:
    def __init__(self):
        print('bar')

Let's talk about the class Foo: part first. This is where we create a class named "Foo". That's it. No more black magic here. We are literally just telling Python that we want to create a class called "Foo".

Now for the next line, def __init__(self):. This is called the class constructor. Almost every class you'll ever write has this part. I'm leaving out a few details here since they are way out of scope of this discussion. This particular function is called the constructor because it, in essence, "constructs" the class. Generally, you don't want to put anything computationally expensive in the constructor nor do you want to call a computationally expensive function from the constructor. Its job is to just set up the instance in such a way that you can use it. The self parameter is always needed and will get passed in automatically when you instantiate the class. To create an instance of the above class, you essentially "call" it like you do a function (i.e. add the parentheses): foo_instance = Foo(). Most classes don't provide much functionality just by existing. You need an instance of the class. There are exceptions and we'll touch on those, so let's continue.

Now let's start building a class that we will continue to build on throughout this tutorial:

1
2
3
class Actor:
    def __init__(self):
        pass
But what if we want to have our player have some actual data? How do we do that? The answer is attributes. We can define class attributes that persist even when the constructor has finished and these attributes can be accessed from other places in your code. So, let's define a few attributes:
1
2
3
4
5
6
class Actor:
    def __init__(self):
        self.health = 100
        self.attack = 5
        self.defense = 0
        self.inventory = ['sword', 'shield']
Alright, let's parse what we see here. When we create a Actor instance, the constructor is going to assign the values health, attack, defense, and some inventory to something called self. The self parameter represents the class instance (or in other words, it's referring to itself), so you're really just assigning those bits of data to the instance of the class. This serves a quite useful purpose. You see, if I didn't prefix all of those with self, then when the constructor finished running the values would get garbage collected away and would not exist anymore. That's not what we want at all. So, we assign them to be instance attributes as above and those values stay in scope for as long as the instance itself exists. Another benefit is that we can change them for individual instances and the changes only apply to that single instance and no others. Here's an example: We can see that all of the attributes we prefixed with self printed just fine, but as soon as we tried to print the value that wasn't prefixed, we got an AttributeError which means that the attribute we tried to access does not exist.

A general rule of thumb for whether something should be an attribute or not is this:

Make it an attribute if you need it later and ONLY if you need it later

Python variable and method names also have a few naming conventions (note that these are common conventions and are not enforced at all in Python). To read the full convention, see PEP 8:

  • No leading underscore
  • Single leading underscore
  • Double leading underscore
  • Trailing underscore

No leading underscores

These are the simplest to understand. They're quite simply things like above: self.health = 100. There are no underscores at the beginning of the attribute name. The purpose of these attributes is that they are intended to be directly accessed and modified by the user.

Single leading underscores

These are a little more complicated to understand. A single leading underscore attribute would look like self._health = 100. The purpose of these attributes is that they are intended to be "protected". The user can still access these, but the leading underscore is the best indication to them that tinkering with this variable could break something because it's not intended to be modified directly. These attributes will be easily accessible under the same name in any subclasses (we will get to those, so be patient).

Double leading underscores

These are the trickiest to understand. One of these attributes would look like
self.__health = 100. These attributes are intended to be private; users are not supposed to access these nor are any subclasses. This doesn't make them inaccessible. They're actually harder to access. This is due to name mangling. When you define one of these attributes on a class, the name of the attribute is changed outside of the class itself. If my class name is Actor with the attribute I listed above, I'd have to access the attribute like instance._Actor__health, but within the Actor class, I could just call self.__health. We'll talk about the subclass implications after we talk about subclasses in the Inheritance section.

Trailing underscore

A common thing that beginners do is to name their attributes something like type or id. While this usually won't get you into trouble and it will usually work fine, this is still frowned upon because type and id have specific meanings in Python. The commonly accepted way to use names like these is to append an underscore to the end of the variable name, so instead of self.id, you should do self.id_. There is really no other purpose to a single trailing underscore in an attribute name, it's just a common way to prevent yourself from accidentally shadowing something else.

Inheritance

Sometimes you want to create multiple classes that have the same basic functionality, but they each behave a bit differently. An example relevant to pygame might be that you have NPCs, Enemies, and the Player. Each one is gonna have pretty much the same basic logic:

  1. making the image
  2. scaling it to the appropriate size (actually, you should ideally make your images the correct size in the first place, but scale if needed)
  3. making sure that you have a way to position the character once it's time to draw it on screen
But maybe each one has different movement logic and possibly the same or different attack logic. It's always a pain to write the same piece of code over and over again. Also, if you change your mind, you'll have to remember every place you need to change your code. This is a great example of how something called subclassing can be useful.

Let's say I want to create NPC and Enemy classes. I might start off with defining an Actor class that doesn't have much functionality, but will serve as the base of my NPC and Enemy classes. That Actor class might be defined in this way:

1
2
3
4
5
class Actor:
    def __init__(self, position, image, size):
        self.image = image
        self.size = size
        self.pos = position
Note that I've put a few more arguments into the constructor: position, image, and size. These are bits of information that I would expect pretty much any character in the game to need. Now, I want to create an NPC class. I could rewrite all of that code in the constructor of my NPC class, but that's a lot of overhead as mentioned before. Instead, let's subclass Actor:
1
2
3
class NPC(Actor):
    def __init__(self, position, image, size, dialogue):
            self.dialogue = dialogue
And that's all there is to it right? Wrong.

Python has no idea what our intentions are. It could assume that we want the constructor of Actor to be called immediately, but that's a lot of overhead and might not actually be what we want. So, we have to explicitly call the constructor of Actor (side note: Actor here is also known as the superclass of NPC, and that'll be important in a second). There are a couple of ways to do that: explicitly calling the constructor of Actor, or by calling the constructor of the superclass.

Calling Actor's constructor:

1
2
3
4
class NPC(Actor):
    def __init__(self, position, image, size, dialogue):
        Actor.__init__(self, position, image, size)
        self.dialogue = dialogue
Calling the superclass constructor:
1
2
3
4
class NPC(Actor):
    def __init__(self, position, image, size, dialogue):
        super().__init__(position, image, size)
        self.dialogue = dialogue

These look practically identical (and in fact, they practically do the exact same thing in simple cases), but there are a few reasons to choose one over the other. Generally, super().__init__ is preferred because it's easy to swap out or rename the superclass if you need to, as well as not needing to remember the self parameter. And for longer class names, it's also shorter. But, the situation where you would possibly want to use Actor.__init__ would be in the case of more complicated inheritance structures (long chains of inheritance or multiple inheritance, both of which are well outside the scope of this article and game design in general, but a link is at the bottom of the page for an excellent video on the topic from the mCoding YouTube channel).

The power of class inheritance is that attributes are inherited from the superclass to the subclass. So, an instance of NPC as given above has the same attributes as Actor (note that this would not be the case if we had not called the constructor of the superclass). Here's an example to play around with: But remember from the beginning what I said about attributes and methods? I said that methods are attributes, so anything that applies to variable attributes also applies to methods! This means that methods are inherited too. Try it in the embed above. Define a method of the Actor class and try to call it from the instance of NPC. One example of that might look like:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
class Actor:
    def __init__(self, position, image, size):
        self.image = image
        self.size = size
        self.pos = position
    
    def talk(self, text):
        print(text)


class NPC(Actor):
    def __init__(self, position, image, size, dialogue):
        super().__init__(position, image, size)
        self.dialogue = dialogue


npc = NPC("Tom", "assets/tom.png")
print(npc.name)
print(npc.image)
npc.talk("Hello World!")

But there's more that we can do! super().__init__() in the constructor of a subclass hints that we can access the methods of the superclass in the subclass! In fact, this is one of the most powerful aspects of inheritance! Let's say my Actor superclass has a method func and I want to run that method in my NPC subclass but I also want it to do some additional stuff. One option would be to copy/paste the code from Actor.func into NPC.func. But there's an alternative that is easier to maintain and reduces the amount of duplicate code: call super().func() in NPC.func, like so:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
class Actor:
    def __init__(self, position, image, size):
        self.image = image
        self.size = size
        self.pos = position
    
    def talk(self, text):
        print(text)


class NPC(Actor):
    def __init__(self, position, image, size, dialogue):
        super().__init__(position, image, size)
        self.dialogue = dialogue
    
    def talk(self, player_text, npc_text):
        print(f"You said {player_text}")
        super().talk(npc_text)

    
npc = NPC((100, 200), "assets/npc.png", (20, 20), "Hello World!")
print(npc.pos)
print(npc.dialogue)
npc.talk("Hello", "Hello there")

Instance Methods

Enter some text and code here

Class Methods

Enter some text and code here

Static Methods

Enter some text and code here

Magic Methods

Enter some text and code here

Properties

Enter some text and code here

Integrating with Pygame

Enter some text and code here


Useful Stuffs

You can visit the pygame documentation to find the full list of functions and other information about the Pygame library.

Other Useful Links



Pygame Examples

Want to contribute to this guide or did you find a problem? Visit us here!

Created by the Pygame Guide for Sneks Organization