The complete guide to Python classes
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:
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):
# 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
.
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.
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 attribute | Class attribute |
Defined in __init__() or a method | Defined directly in the class |
Unique to each object | Shared between all objects |
Access by self.attribute | Access 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.
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.
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
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.
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:
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:
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:
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 byprint()
to display an object;__repr__()
-> used in the interpreter to represent the object;__eq__()
-> comparison with==
;__len__()
-> allows usinglen(obj)
.
Let's take this concrete example:
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:
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:
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 type | Main parameter | Access to instance / class | Usage |
Instance | self | Instance | Read / Modify attributes |
Class | cls | Class | Alternative constructor (@classmethod ) |
Static | None | None | Utility 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. 😅
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:
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:
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:
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?
Criterion | Inheritance | Composition |
Logical relationship | "is a" (A Dog is an Animal) | "has a" (A car has an engine) |
Coupling | Strong | Weak |
Flexibility | Lesser | High |
Reuse | By inheritance | By 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:
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:
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! 👀
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:
@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:
class InsufficientBalanceError(Exception):
"""Exception raised when balance is insufficient."""
pass
And here's how to integrate it into a class:
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.