Believemy logo purple

The complete guide to Python classes

A class is used in Python to create a model that can be used to create objects: this is known as object-oriented programming.
Believemy logo

In Python, the class keyword allows you to define what is called a class, which is a structure that models a concept or object from the real world.

Classes are a fundamental pillar of object-oriented programming (abbreviated as OOP), a paradigm widely used in medium and large-scale Python projects.

Creating a class amounts to defining a generic model that can then be instantiated multiple times. These instances are called objects.

A class is like an architect's blueprint. The object is the house built from that blueprint.

 

Definition of a class

A class is an abstraction. It groups data (called attributes) and behaviors (called methods) within the same logical structure.

For example, a Car class could group:

  • Attributes: brand, model, color;
  • Methods: start(), accelerate(), brake().

The class therefore allows you to model an entity in a clean and reusable way, as we have seen.

Each time we create an object from a class, we speak of an instance of this class. These objects can interact with each other, be stored in lists, modified, copied, etc.

 

Basic syntax of a class

Here is the minimal syntax to create a class in Python:

PYTHON
class Animal:
    def __init__(self, name):
        self.name = name

    def speak(self):
        print(f"{self.name} makes a sound.")

Let's decrypt what this example does! 😋

First, we have the class keyword: it tells Python that we are creating one.

Next, we give it its name: Animal.

A class must have its name in PascalCase format to respect conventions.

Finally, we use the __init__() method which is called automatically when creating an instance. This is what we call a constructor.

Also note that self refers to the current object.

 

Creating an instance in Python

Once a class is defined, we can create an instance (that is, a unique object based on this model):

PYTHON
# What we had already created
class Animal:
    def __init__(self, name):
        self.name = name

    def speak(self):
        print(f"{self.name} makes a sound.")

# Here: we create our object
my_dog = Animal("Rex")
my_dog.speak()  # Rex makes a sound.

What happens in this example:

  • Animal("Rex") automatically calls the __init__() method;
  • self.name = name stores the name in the object;
  • speak() uses this attribute to display a sentence.

 

Instance attributes vs class attributes

It is essential to distinguish between the two types of attributes, namely instance attributes and class attributes.

Instance attributes

They are specific to each object, defined via self.

PYTHON
class Car:
    def __init__(self, brand):
        self.brand = brand

Each car will have its own brand.

 

Class attributes

They are shared by all instances (so all objects) of the class.

PYTHON
class Car:
    wheels = 4  # class attribute

print(Car.wheels)  # 4

Class attributes are useful for defining constants that are common to all instances.

 

Comparison table

Instance attributeClass attribute
Defined in __init__() or a methodDefined directly in the class
Unique to each objectShared between all objects
Access by self.attributeAccess by Class.attribute or self.attribute

 

Methods of a class

A method is a function defined inside a class.

It can:

  • Access the object's data (self.attribute);
  • Modify internal values;
  • Perform an action related to the object's state.
PYTHON
class Account:
    def __init__(self, holder, balance):
        self.holder = holder
        self.balance = balance

    def deposit(self, amount):
        self.balance += amount
        print(f"${amount} deposited. New balance: ${self.balance}")

a = Account("Alice", 100)
a.deposit(50)  # $50 deposited. New balance: $150

Methods make the class dynamic and interactive. They allow objects to come alive. 👀

 

The principle of encapsulation and visibility

Encapsulation is a fundamental principle in object-oriented programming. It consists of hiding internal details of an object so as not to expose sensitive or unnecessary data outside the class.

Generally speaking, Python does not manage data protection in the literal sense. But there are conventions to immediately see when data can be used or not:

  • name: accessible everywhere (public);
  • _name: signaled as protected, internal use recommended;
  • __name: private, "mangled" name to avoid collisions.

Let's take a small example.

PYTHON
class Bank:
    def __init__(self, holder):
        self._balance = 0      # protected
        self.__holder = holder  # private

    def show_balance(self):
        return self._balance

Here, even though _balance is technically accessible, it is conventionally reserved for internal use. The double underscore __ prevents direct access from outside (bank.__holder raises an error).

 

Classes and inheritance

Inheritance allows you to create a new class from an existing class. This allows you to reuse and specialize common behaviors.

It's a powerful way to reuse code while customizing certain behaviors.

Simple inheritance example

PYTHON
class Animal:
    def speak(self):
        print("The animal makes a sound.")

class Dog(Animal):
    pass

rex = Dog()
rex.speak()  # Displays: The animal makes a sound.

The Dog class doesn't have its own speak() method, so it inherits directly from that of Animal.

 

Redefining a method (overriding)

We can modify (or "redefine") an inherited method so that it has different behavior in the child class. This is called overriding.

PYTHON
class Animal:
    def speak(self):
        print("The animal makes a sound.")

class Dog(Animal):
    def speak(self):
        print("The dog barks.")

rex = Dog()
rex.speak()  # Displays: The dog barks.

This version replaces the speak() method of the Animal class.

 

Using super() to call the parent method

Sometimes, we want to add behavior while preserving that of the parent class. This is where super() comes in:

PYTHON
class Cat(Animal):
    def speak(self):
        super().speak()  # Calls speak() from Animal
        print("The cat meows.")

super() is very useful when you want to chain calls through a class hierarchy, especially in multiple inheritance.

 

Nested classes

In Python, you can define a class inside another. We then speak of a nested class.

This allows you to group logically related structures under the same roof.

Here's a small example with a nested class:

PYTHON
class Car:
    def __init__(self, brand):
        self.brand = brand

    class Engine:
        def __init__(self, engine_type):
            self.engine_type = engine_type

And we use the nested class like this:

PYTHON
engine = Car.Engine("hybrid")
print(engine.engine_type)  # hybrid

We can for example use a nested class when the internal class makes no sense outside the external class.

Avoid using a nested class if you need it to be used in multiple contexts. Additionally, using too many nested classes can sometimes greatly reduce the readability of the entire project. 😉

 

Special methods of classes

Special methods, also called "dunder methods 🇺🇸" (for "double underscore"), are functions that have a name framed by two underscores, like __init__, __str__, etc.

Python calls them automatically in particular contexts: object creation, printing, comparison, etc.

Here are the most frequent methods:

  • __init__() -> called when creating an instance;
  • __str__() -> used by print() to display an object;
  • __repr__() -> used in the interpreter to represent the object;
  • __eq__() -> comparison with ==;
  • __len__() -> allows using len(obj).

Let's take this concrete example:

PYTHON
class Book:
    def __init__(self, title, author):
        self.title = title
        self.author = author

    def __str__(self):
        return f"{self.title} written by {self.author}"

b = Book("1984", "George Orwell")
print(b)  # 1984 written by George Orwell

Special methods therefore make our objects more natural to use in the Python language.

 

Class methods and static methods

In Python, in addition to instance methods (which use self as we've seen 😋), we can define two other types of methods within a class:

  • Class methods (@classmethod);
  • And static methods (@staticmethod).

Class methods

It receives the class itself as the first argument, conventionally named cls.

This allows in particular to create alternative constructors or functions that concern the class as a whole (and not a particular instance).

Example:

PYTHON
class User:
    def __init__(self, name):
        self.name = name

    @classmethod
    def create_guest(cls):
        return cls("Guest")

guest = User.create_guest()
print(guest.name)  # Guest

Here, create_guest is a method that returns a predefined instance of the class. It uses cls to construct the object, which makes it compatible even with subclasses.

Use @classmethod when you need to construct or manipulate a class without going through an instance.

 

Static methods

It takes no implicit argument. It's simply a function placed in a class for organizational purposes.

Example:

PYTHON
class Math:
    @staticmethod
    def square(x):
        return x * x

print(Math.square(5))  # 25

In this example the method depends neither on an object nor on the class: it's a purely utility function, placed there to group the logic.

 

Comparison table

Method typeMain parameterAccess to instance / classUsage
InstanceselfInstanceRead / Modify attributes
ClassclsClassAlternative constructor (@classmethod)
StaticNoneNoneUtility methods (@staticmethod)

 

Multiple inheritance on classes

Multiple inheritance means that a class can inherit from several parent classes at once. This gives flexibility, but also requires a little more attention. 😅

PYTHON
class Speaks:
    def speak(self):
        print("I speak.")

class Moves:
    def move(self):
        print("I move.")

class Robot(Speaks, Moves):
    pass

r2d2 = Robot()
r2d2.speak()  # I speak.
r2d2.move()  # I move.

Here, the Robot class inherits methods from both classes, without any redefinition. It's simple and efficient.

If two parent classes define a method with the same name, Python follows a strict order to decide which one to call: this is the MRO, or method resolution order.

Conflict example:

PYTHON
class A:
    def display(self):
        print("A")

class B:
    def display(self):
        print("B")

class C(A, B):
    pass

c = C()
c.display()  # A

Python calls the display() method of the leftmost class in the declaration (A here).

You can display this order with the __mro__ method:

PYTHON
print(C.__mro__)
# (<class '__main__.C'>, <class '__main__.A'>, <class '__main__.B'>, <class 'object'>)

 

Composition vs inheritance: which approach to choose?

Inheritance is not the only way to structure an object-oriented program.

Python also offers another fundamental principle: composition, which is based on the idea of "having a" instead of "being a" (it's a bit twisted).

What is composition?

Composition consists of integrating an object into another object, rather than inheriting from its class.

Example:

PYTHON
class Engine:
    def start(self):
        print("The engine starts.")

class Car:
    def __init__(self):
        self.engine = Engine()

    def drive(self):
        self.engine.start()
        print("The car drives.")

Here, the Car class contains an object of type Engine. It does not extend it, but uses it to accomplish its task.

 

Composition vs inheritance: what to choose?

CriterionInheritanceComposition
Logical relationship"is a" (A Dog is an Animal)"has a" (A car has an engine)
CouplingStrongWeak
FlexibilityLesserHigh
ReuseBy inheritanceBy delegation

Use inheritance when the relationship is natural ("a square is a rectangle")

Use composition when you assemble functionalities ("a car has a GPS").

Easy, right? 👀

 

Typing and annotations of classes

Python allows you to annotate the types of attributes and parameters for more clarity, especially with the help of tools like mypy, pylance or IDEs like PyCharm.

Here's an example of typing in a class:

PYTHON
class Product:
    name: str
    price: float

    def __init__(self, name: str, price: float):
        self.name = name
        self.price = price

Thanks to these annotations, we immediately know what to expect and development tools can automatically check type consistency.

We specify for example:

  • That name is a string (str);
  • That price is a float (float) - so a decimal number.

We can also type methods:

PYTHON
def calculate_tax(self, rate: float) -> float:
    return self.price * rate

Here rate expects to also receive a float, calculate_tax must return a float too (-> float)!

Finally, annotations don't change how the program works (they are not mandatory at runtime), but they strengthen documentation and maintainability.

 

Dataclasses, an alternative for classes

Introduced in Python 3.7 via the dataclasses module, dataclasses offer a simple, clean and concise way to create classes containing mostly data.

Creating a classic class with attributes, constructor, special methods (__repr__, __eq__, etc.) can be verbose.

Dataclasses do all this automatically! 👀

PYTHON
from dataclasses import dataclass

@dataclass
class Book:
    title: str
    author: str
    pages: int

book1 = Book("1984", "Orwell", 328)
print(book1)
# Book(title='1984', author='Orwell', pages=328)

No need to write __init__, __repr__ or __eq__: everything is generated automatically.

But that's not all, it's possible to add options to make a class immutable or to sort it automatically:

PYTHON
@dataclass(frozen=True, order=True)
class Product:
    name: str
    price: float

In this example:

  • frozen=True makes the object immutable (like a tuple);
  • order=True allows sorting objects (<, >, etc.).

 

Classes and exceptions

Creating our own custom exceptions allows us to finely control errors in object-oriented code, while keeping clear messages.

Here's an example of exception:

PYTHON
class InsufficientBalanceError(Exception):
    """Exception raised when balance is insufficient."""
    pass

And here's how to integrate it into a class:

PYTHON
class Account:
    def __init__(self, balance):
        self.balance = balance

    def withdraw(self, amount):
        if amount > self.balance:
            raise InsufficientBalanceError("Insufficient funds.")
        self.balance -= amount

By creating our own exceptions, we facilitate debugging and code understanding.

 

Frequently asked questions about classes

When you start with classes (and even after!) you can ask yourself many questions... Let's try to see the most frequent ones.

What is self for in a class?

self is a reference to the current instance of an object. It allows access to its attributes and calling its methods from inside the class.

 

Do you always have to use __init__?

Not always! If you use a @dataclass, Python creates __init__ for you.

Otherwise, __init__ is essential to initialize attributes.

 

Can you have multiple classes in the same file?

Yes, and it's common. But make sure the file remains readable: one main class per file is a good practice if the project becomes complex.

 

How to learn Python?

With a complete course.

Discover our python glossary

Browse the terms and definitions most commonly used in development with Python.