Back to articles list Articles
11 minutes read

Simple Steps for Creating Your Own Class in Python

Do you know how to build your own class in Python? Writing custom classes and custom objects in Python makes your code clearer, more readable, and easier to maintain.

Before we start, if you still need some good reasons to learn Python, Rebecca can help you here.

The concept of object-oriented programming emerged in the '60s, but its popularity did not start growing until the '90s. Today, object-oriented programming is everywhere and is an essential programming paradigm to understand.

Object-oriented programming is about creating custom objects. An object is a group of interrelated functions and variables interacting together. If you are not familiar with the concept of functions, Kateryna talks about it in detail here.

In contrast to procedure-oriented programming, object-oriented programming reduces code complexity, making it clearer and easier to maintain. It also makes it possible to hide data through encapsulation. Procedure-oriented programming lacks this security, as all functions can access data. Object-oriented programming can be a bit challenging, and I recommend you go through our Python programming track.

In this article, I guide you through what a custom class is in Python and how you can create one using constructors. Then, I explain how to define class attributes and different types of methods. Finally, after a word on the visibility of Python custom classes, you learn how to compare and perform operations on Python custom objects.

Creating a Custom Class in Python Using a Constructor

A class is a collection of objects. It is a data structure defined by the user, created with the keyword class to keep related things together. So, a class is a grouping of object-oriented constructs.

Let's write a simple empty class:

class Pokemon: 
	Pass

# instantiate the class Pokemon and assign it to a variable pokemon
pokemon = Pokemon()
print(pokemon)

The output:

<__main__.Pokemon object at 0x0000027B56ADD730>

Because our Python custom class is empty, it simply returns the address where the object is stored.

In object-oriented programming, the properties of a custom object are defined by attributes, while its methods define its behavior. There are three types of methods:

  • Instance methods
  • Class methods
  • Static methods

In Python, the self keyword represents an instance of a class. It works as a handle to access the class members, such as attributes from the class methods. It is the first argument to the __init__() method and is called automatically to initialize the class attributes with the values defined by the user.

Let's run an example:

class Pokemon:

    def __init__(self): 
        print("calling __init__() constructor...")

pokemon = Pokemon()

The output:

calling __init__() constructor...

However, a custom class in Python is useless if it is not associated with functionalities. Functionalities are added using attributes and act as containers for data and functions for those attributes. These functions are called methods.

Instance and Class Attributes in a Python Custom Class

Let's update the Pokemon class with an init() method that creates name and age attributes. These attributes are called instance attributes.

class Pokemon:
    def __init__(self, name, attack):
        self.name = name 
        self.attack = attack

Now, let's define a class attribute for our Pokemon class:

class Pokemon:
    # Class attribute
    species = "Mouse"
    def __init__(self, name, attack): 
        self.name = name
        self.attack = attack

We use class attributes to define properties with the same value for every class instance and instance attributes for properties that vary from one instance to another.

Let's create some Pokémons.

class Pokemon:
    # Class attribute
    species = "Mouse"
    def __init__(self, name, attack): 
        self.name = name
        self.attack = attack
        
pikachu = Pokemon("Pikachu", "Double Kick")
raichu = Pokemon("Raichu", "Thunder Punch")

After creating the Pokemon instances, we can access their instance attributes using the dot notation, [instance name].[attribute name], like these:

>>> pikachu.name
'Pikachu'
>>> pikachu.attack
'Double Kick'
>>> pikachu.species
'Mouse'
>>> raichu.name
'Raichu'
>>> raichu.attack
'Thunder Punch'

One of the main benefits of organizing data with classes is that instances are guaranteed to have the expected attributes. However, it does not mean we cannot change their value dynamically, for instance:

>>> pikachu.attack = "Thunder Shock"
>>> pikachu.attack
'Thunder Shock'

Instance Methods in Python Custom Classes

Instance methods are functions defined inside a class and can only be called from an instance of that class. Like __init__(), the first parameter of an instance method is always self.

Let's define some instance methods for our Python custom class Pokemon.

class Pokemon:
    # Class attribute
    species = "Mouse"
    def __init__(self, name, attack): 
        self.name = name
        self.attack = attack

    # One instance method
    def description(self):
        return f"{self.name} favorite attack is {self.attack}"

    # A second instance method
    def speak(self, sound):
        return f"{self.name} says {sound}"

The self keyword is essential. Without it, we cannot access the attributes and the methods of a custom class in Python, and it results in an error. In other words, it binds the attributes with the given arguments.

Let's use our new instance methods by creating a new Pokemon instance dynamically:

>>> pichu = Pokemon("Pichu", "Nuzzle")
>>> pichu.description()
"Pichu favorite attack's is Nuzzle"
>>> pichu.speak("pichu pichu")
'Pichu says pichu pichu'

In the above Pokemon class, the description() method returns a string containing information about the Pokemon instance pichu. When we write a Python custom class, it's a good idea to have a method that returns a string containing helpful information about the instance of the class.

Class Methods in Python Custom Classes

The class method exists to set or get the status of a class. They can't access or modify specific instance data. Methods are there to describe the behavior of the objects and are defined inside a class.

A class method must be defined using the @classmethod decorator. They also take one default parameter cls, which points to the class. It is not mandatory to name it cls, but it is good to follow conventions.

Class methods are used to create a factory method. Factory methods return different class objects depending on the use case.

Let's continue with Pokemon:

class Pokemon:
    def __init__(self, names):
        self.names = names

    def __repr__(self):
        return f'Pokemon({self.names})'

    @classmethod
    def mouse(cls):
        return cls(['Pichu', 'Pikachu', 'Raichu'])

    @classmethod
    def hummingbird(cls):
        return cls(['Florabri', 'Floressum'])

Instead of calling the Pokemon constructor directly, I use the cls argument in the mouse and hummingbird class methods. When I change the class name, I do not have to update the constructor name in each class method.

__repr__() is used to represent an object of a class as a string. It means the output is a string representation of the object. Without it, the output of Pokemon.mouse() is:

>>> Pokemon.mouse()
<__main__.Pokemon at 0x1d219dcb4f0>

This is what these class methods do:

>>> Pokemon.mouse()
Pokemon(['Pichu', 'Pikachu', 'Raichu'])
>>> Pokemon.hummingbird()
Pokemon(['Florabri', 'Floressum'])

So, we have used class methods to create new Pokemon objects already configured the way we want.

Static Methods in Python Custom Classes

Static methods cannot access the class data because they are self-sufficient and can work on their own. They are not attached to any class attribute, so they cannot get or set the instance state or class state.

To define them, we need to use the @staticmethod decorator. Unlike the instance and class methods, we do not need to pass any default parameter.

Static functions are used to create utility functions for performing routine programming tasks. Let's write an example in which we have a static method to calculate the damages done by a Pokémon attack:

class Pokemon: 
    def __init__(self, power, level, names):
        self.power = power
        self.level = level
        self.names = names
        
    def __repr__(self):
        return (f'Pokemon({self.power}, '
                f'{self.level}, '
                f'{self.names})')
    
    def total_damage(self):
        return self.damage(self.power, self.level)

    @staticmethod
    def damage(power, level):
        return (power * level * 2) / 50

I modified the constructor to accept the power and level arguments and __repr__() to display it. I also added a total_damage() instance method that calculates and returns the damages when the Pokémon attacks. And, instead of calculating the level of damages directly within total_damage(), I added a formula to compute the damages in a separate damage() static method.

Let's try it:

>>> charmander = Pokemon(20, 8, "Charmander")
>>> charmander.total_damage()
6.4
>>> charmander.damage(20, 8)
6.4

The use case here is very straightforward. Static methods don't have access to cls or self. They behave like regular functions but belong to the namespace of the class and are usually not tied to an object lifecycle. The above damage() method is completely independent of the class, making testing much more manageable.

In addition, we don't have to worry about setting up a complete class instance before testing the method in a unit test. We proceed like we would if we were testing a regular function, making future maintenance easier.

To reinforce this point, let's see what happens if we try to call these methods on the class itself without creating an instance of the class:

class NewClass:
    def method(self):
        return 'Calling instance method...', self

    @classmethod
    def classmethod(cls):
        return 'Calling class method...', cls

    @staticmethod
    def staticmethod():
        return 'Calling static method...'
>>> NewClass.method()
TypeError: method() missing 1 required positional argument: 'self'
>>> NewClass.classmethod()
('Calling class method...', __main__.NewClass)
>>> NewClass.staticmethod()
'Calling static method...'

We were able to call classmethod() and staticmethod(), but the attempt to call the instance method method() failed with a TypeError. This is because we tried to call an instance function directly on the class blueprint itself without creating an instance of the class. Python could not populate the self argument, resulting in call failure. That we can call staticmethod() without issue, in contrast, proves the method is completely independent of the rest of the class.

Visibility in Python Custom Classes

Object-oriented programming languages like C++ and Java control access to classes with the public, private, and protected keywords. Python conceptualizes public, protected, and private access modifiers, unlike other languages like C#, Java, and C++.

Public Members of a Custom Class in Python

Public members can be accessed from outside the class. This means we can freely modify the class attributes without any restrictions. The same class object is required to invoke a public method. This is done to follow the principle of data encapsulation. In Python, class members are public by default.

Protected Members of a Custom Class in Python

Protected members of a class are accessible from within the class and are also available to its subclasses.

Python does not have any mechanism for restricting access to any instance variable or method. Instead, it has a convention of prefixing the name of the variable or method with a single or double underscore to emulate the behavior of protected and private access specifiers. For protecting an instance variable, a single-underscore prefix (“_”) is added, thus preventing it from being accessed unless within a subclass.

Note this does not prevent instance variables from accessing or modifying the instance.

Private Members of a Custom Class in Python

Private members of the class cannot access the environment from outside the class and can be handled only from within the class itself. Any attempt to alter the variable results in an AttributeError.

A private member is named by adding a double-underscore prefix (“__”) before the name of the variable. Python performs name mangling of private variables, and every member with a double underscore is changed to _object._class__variable. So, it can still be accessed from outside the class, but the practice should be avoided.

Comparison Methods of two Python Custom Objects

In coding, we use operators such as >. However, you need to use __gt__() and the likes to implement your Python custom class functionality.

The methods below are used for the comparison of the attributes of an object. To help remember them, look at the capitalization in the comments.

class Value:
    def __init__(self, baz):
        self.baz = baz
    # Less Than operator
    def __lt__(self, obj2):
        return self.baz < obj2.baz
    # Greater Than operator
    def __gt__(self, obj2):
        return self.baz > obj2.baz
    # Less than or Equal operator
    def __le__(self, obj2):
        return self.baz <= obj2.baz
    # Greater than or Equal operator
    def __ge__(self, obj2):
        return self.baz >= obj2.baz
    # EQual operator
    def __eq__(self, obj2):
        return self.baz == obj2.baz
    # unequal (Not Equal) operator
    def __ne__(self, obj2):
        return self.baz != obj2.baz
foo = Value(6)
bar = Value(9)
print(
    foo < bar,
    foo > bar,
    foo <= bar,
    foo >= bar,
    foo == bar,
    foo != bar
)

The output:

True False True False False True

The instances foo and bar contain an attribute named foo that holds integer values 6 and 9, respectively. This is a very simple example. Your methods can use more advanced operations; e.g., they could compare a few different attributes at once.

Mathematical Operations on two Python Custom Objects

It is also possible to perform mathematical operations on Python custom objects. Following the structure of the previous snippet about comparison methods, below is a snippet to perform mathematical operations on two Python custom objects:

class Value:

    def __init__(self, baz):
        self.baz = baz

    # Adding two objects 
    def __add__(self, obj2):
        return self.baz + obj2.baz

    # Subtracting two objects    
    def __sub__(self, obj2):
        return self.baz - obj2.baz

    # Multiplying two objects    
    def __mul__(self, obj2):
        return self.baz * obj2.baz

    # Dividing two objects    
    def __truediv__(self, obj2):
        return self.baz / obj2.baz

    # Get the remainder of a division of two objects    
    def __mod__(self, obj2):
        return self.baz % obj2.baz
        
foo = Value(2)
bar = Value(4)

print(
    foo + bar,
    foo - bar,
    foo * bar,
    foo / bar,
    foo % bar,
)

Which gives the output:

6 -2 8 0.5 2

For more information about Python classes, check the documentation here.

Closing Thoughts on the Custom Class in Python

We’ve covered a lot of ground in this introductory article on creating Python custom classes and objects. I’ve chosen not to address the topic of destructors, which are called when an object gets destroyed. Unlike other programming languages such as C++, Python has a garbage collector with automatic memory management, making destructors less necessary.

This is a complex topic, and we have barely scratched the surface. I highly recommend practicing your Python skills by playing with the above snippets and going through the exercises in our Python programming track. You can also check Dorota's excellent list of resources for learning Python.

In the next article, we explore how to write modules in Python. In the meantime, do not forget to visit LearnPython.com!