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 = namestores 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 = brandEach 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._balanceHere, 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_typeAnd we use the nested class like this:
engine = Car.Engine("hybrid")
print(engine.engine_type) # hybridWe 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() # APython 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 = priceThanks to these annotations, we immediately know what to expect and development tools can automatically check type consistency.
We specify for example:
- That
nameis a string (str); - That
priceis a float (float) - so a decimal number.
We can also type methods:
def calculate_tax(self, rate: float) -> float:
return self.price * rateHere 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: floatIn this example:
frozen=Truemakes the object immutable (like a tuple);order=Trueallows 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."""
passAnd 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 -= amountBy 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.