POO en Python : classes, self, héritage et dataclasses

Écrivez vos propres classes de façon pratique — la méthode init et self, un repr lisible, l’héritage avec super(), une property pour les attributs calculés, le piège attribut de classe vs attribut d’instance, et les dataclasses pour supprimer le boilerplate — chaque point expliqué par une sortie que vous pouvez exécuter, plus le jugement honnête « avez-vous vraiment besoin d’une classe ? ».

La programmation orientée objet en Python à partir de zéro : ce que sont vraiment une classe, un objet, une instance, une méthode et un attribut ; la méthode init et self ; un repr lisible ; l’héritage avec super() et isinstance ; des attributs calculés avec une property ; le piège attribut de classe vs attribut d’instance (une liste partagée entre les instances) et sa correction ; les dataclasses pour supprimer le boilerplate avec field(default_factory=list) pour les valeurs mutables par défaut ; et le jugement honnête sur le moment où vous avez vraiment besoin d’une classe plutôt que d’une simple fonction ou d’un dict.

Date de publication

18 juillet 2026

Modifié

18 juillet 2026

AstucePoints clés
  • Une classe regroupe des données et le comportement qui agit dessus. Lorsque quelques valeurs et les fonctions qui les manipulent voyagent toujours ensemble — un compte bancaire possède un solde et deposit/withdraw — une classe les emballe en une seule chose. Vous appelez la classe pour créer une instance (un objet) ; ses valeurs stockées sont ses attributs et ses fonctions sont ses méthodes.
  • __init__ met en place chaque nouvel objet, et self est cet objet. __init__(self, …) s’exécute une fois par instance pour stocker ses attributs de départ ; self est le premier paramètre de chaque méthode et désigne l’objet précis sur lequel la méthode a été appelée. Oublier self comme premier paramètre est l’erreur de débutant la plus courante.
  • L’héritage réutilise une classe de base ; gardez-le sur un seul niveau et appelez super(). Une sous-classe récupère gratuitement les méthodes du parent et peut ajouter ou redéfinir les siennes ; super().__init__(…) exécute la mise en place du parent pour que l’état de base soit initialisé. Les arbres d’héritage profonds sont un anti-pattern — préférez la composition (contenir un objet) à l’empilement de nombreux niveaux.
  • Le piège attribut de classe vs attribut d’instance mord tout le monde une fois. Un attribut affecté dans le corps de la classe est partagé par toutes les instances ; s’il est mutable (une liste, un dict), les modifications fuient alors d’un objet à l’autre. Affectez l’état propre à chaque objet dans __init__ (self.items = []), et pour une @dataclass utilisez field(default_factory=list).
  • @dataclass écrit le boilerplate à votre place — et souvent vous n’avez pas besoin de classe du tout. À partir de quelques champs typés, elle génère automatiquement __init__, __repr__ et __eq__ ; c’est le choix moderne par défaut pour les classes qui portent des données. Mais une classe ne se justifie que lorsque données et comportement voyagent ensemble, ou lorsque vous avez plusieurs instances avec état — sinon une simple fonction ou un dict est plus clair.

Introduction

Certaines données et les opérations qui les accompagnent vont naturellement ensemble. Un compte bancaire n’est pas seulement un solde — c’est un solde plus les choses que vous en faites : deposit, withdraw, « affiche-moi le solde ». Un point du plan, c’est un x et un y plus « à quelle distance est-il de l’origine ? ». Lorsqu’une poignée de valeurs et les fonctions qui agissent sur elles se retrouvent sans cesse passées en groupe, c’est le signal qu’il faut recourir à une classe — la façon dont Python emballe données et comportement en une seule unité.

Un peu de vocabulaire, défini une fois pour toutes. Une classe est un plan : elle décrit les données que contient un objet et ce qu’il peut faire. Un objet (ou instance) est une chose concrète construite à partir de ce plan — on en crée un en appelant la classe, comme Account("Ada"). Les valeurs qu’un objet stocke sont ses attributs (les données), et les fonctions définies à l’intérieur de la classe sont ses méthodes (le comportement). Voilà la programmation orientée objet (POO) en une phrase : regrouper attributs et méthodes derrière une classe, puis en créer des instances.

Cette leçon construit des classes de façon pratique, du plus petit morceau utile d’abord. Nous commençons par une classe vue comme un plan (class, __init__, self), y ajoutons un __repr__ lisible, puis en héritons avec super(). Nous couvrons @property (un attribut calculé), le piège attribut de classe vs attribut d’instance qui fait trébucher tout le monde, et @dataclass — le raccourci moderne qui écrit le boilerplate à votre place. Nous terminons par la question honnête que les tutoriels esquivent : avez-vous vraiment besoin d’une classe ici, ou une fonction suffit-elle ? Chaque résultat ci-dessous a été produit par le code montré, et les blocs s’enchaînent dans l’ordre. Pour expérimenter, modifiez la cellule Essayez en direct près de la fin et appuyez sur Run.

Une classe est un plan

Une classe décrit les données que contient un objet et les méthodes qu’il peut exécuter. La méthode spéciale __init__ (abréviation d’initialise) s’exécute une fois, automatiquement, chaque fois que vous créez une instance — son rôle est de stocker les attributs de départ de cette instance. Son premier paramètre, nommé par convention self, est le nouvel objet ; vous lui attachez des attributs avec self.name = value. Toutes les autres méthodes prennent elles aussi self en premier, afin de pouvoir lire et modifier l’objet sur lequel elles ont été appelées. Voici un petit Account :

class Account:
    def __init__(self, owner, balance=0):   # runs when you create an Account
        self.owner = owner                   # store attributes ON this instance (self)
        self.balance = balance               # `balance=0` is the default starting value

    def deposit(self, amount):               # a METHOD: `self` is the account it's called on
        self.balance += amount
        return self.balance

a = Account("Ada", 100)                      # make an INSTANCE — __init__ runs with owner="Ada"
print(a.owner)
print(a.deposit(50))                         # call the method on THIS account
Ada
150

L’appel Account("Ada", 100) a créé un objet et exécuté __init__, qui y a stocké owner="Ada" et balance=100. Lire a.owner renvoie Ada. Puis a.deposit(50) a exécuté la méthode deposit avec self lié à a : elle a ajouté 50 au solde de ce compte et renvoyé le nouveau total, 150. Remarquez que vous ne passez jamais self vous-même — a.deposit(50) le fournit automatiquement ; vous écrivez la valeur (50) et Python remplit l’objet (a). Un second compte, Account("Bo"), serait une instance indépendante avec son propre balance distinct — la classe est le plan, chaque objet en est une copie remplie à part.

Un objet lisible : __repr__

Affichez le compte tel quel et Python ne vous dit presque rien d’utile :

print(repr(a))     # the default: identity, not contents
<__main__.Account object at 0x105a835c0>

Par défaut, c’est quelque chose comme <__main__.Account object at 0x…> — le type et une adresse mémoire, ce qui est inutile dans un débogueur, un log ou une session interactive. Définissez la méthode spéciale __repr__ pour renvoyer une chaîne précise et lisible représentant l’objet. La convention est de renvoyer quelque chose qui ressemble au code qui le recréerait :

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

    def deposit(self, amount):
        self.balance += amount
        return self.balance

    def __repr__(self):                          # what `repr()` and the REPL show
        return f"Account({self.owner!r}, {self.balance})"

a = Account("Ada", 100)
a.deposit(50)
print(repr(a))
Account('Ada', 150)

Maintenant repr(a) renvoie Account('Ada', 150) — le nom de la classe, le propriétaire et le solde courant, sous une forme que vous pourriez recoller pour reconstruire un objet équivalent. (Le !r de la f-string applique repr à owner, ce qui explique pourquoi 'Ada' affiche ses guillemets.) Un bon __repr__ est l’une des quelques lignes les plus rentables que vous puissiez ajouter à une classe : chaque ligne de log, message d’erreur et vue de l’objet dans le débogueur devient instantanément lisible.

L’héritage et super()

Souvent, un nouveau type est une spécialisation d’un type que vous possédez déjà. Un compte épargne est un compte — il a un propriétaire, un solde et accepte les dépôts — mais il rapporte aussi des intérêts. L’héritage permet à une sous-classe de réutiliser tout d’une classe de base et de n’ajouter ou modifier que ce qui diffère. Vous écrivez class Savings(Account) pour hériter, et appelez super().__init__(…) pour exécuter la mise en place du parent afin que les attributs hérités soient initialisés :

class Savings(Account):                          # Savings IS AN Account (inherits everything)
    def __init__(self, owner, balance=0, rate=0.05):
        super().__init__(owner, balance)         # run Account.__init__ to set owner + balance
        self.rate = rate                         # then add what's specific to Savings

    def add_interest(self):                      # a NEW method Account doesn't have
        self.balance += self.balance * self.rate
        return self.balance

s = Savings("Bo", 1000)
print(s.deposit(0))        # inherited from Account, unchanged
print(s.add_interest())    # Savings-specific behaviour
print(isinstance(s, Account))
1000
1050.0
True

Savings n’a pas redéfini deposit — elle en a hérité, donc s.deposit(0) renvoie le solde 1000 en utilisant la méthode du parent. add_interest est propre à Savings : elle ajoute 5 % et renvoie 1050.0. L’appel super().__init__(owner, balance) est l’habitude importante — il délègue à Account.__init__, qui définit self.owner et self.balance ; omettez-le et ces attributs ne seraient jamais créés (voir Problèmes fréquents). Enfin, isinstance(s, Account) vaut True : un Savings est réellement un Account, il fonctionne donc partout où un compte est attendu.

Gardez l’héritage peu profond — un seul niveau, comme ici. Les hiérarchies profondes (une sous-classe d’une sous-classe d’une sous-classe) sont un anti-pattern bien connu : elles dispersent le comportement dans de nombreux fichiers et rendent difficile de savoir quelle classe définit quoi. Quand vous êtes tenté de continuer à sous-classer, préférez plutôt la composition — faites en sorte qu’un objet contienne un autre objet en attribut au lieu d’en hériter. « A-un » est généralement plus souple que « est-un ».

Attributs calculés : @property

Certains attributs ne sont pas stockés — ils sont calculés à partir d’autres attributs. Un cercle stocke son rayon ; son aire en est dérivée. Vous pourriez écrire une méthode area(), mais les appelants devraient alors penser aux parenthèses. Le décorateur @property permet d’accéder à une méthode comme à un attribut, sans parenthèses :

import math

class Circle:
    def __init__(self, radius):
        self.radius = radius

    @property
    def area(self):                    # a method that behaves like a read-only attribute
        return math.pi * self.radius ** 2

c = Circle(2)
print(round(c.area, 2))                # note: c.area, NOT c.area()
12.57

c.area s’obtient sans parenthèses — cela exécute la méthode et renvoie la valeur calculée, 12.57 pour un rayon de 2 — tout en se lisant comme un simple accès à un attribut. C’est tout l’intérêt de @property : une valeur dérivée à la demande mais qui ressemble à une donnée stockée, si bien que l’interface reste propre et que les appelants n’ont pas besoin de savoir qu’elle est calculée.

@property est aussi la façon dont Python gère l’encapsulation — l’idée de « garder l’état interne à l’intérieur ». Python n’a pas d’attributs vraiment privés ; il existe en revanche une convention forte : un underscore en préfixe (self._radius) marque un attribut comme interne — à ne pas toucher de l’extérieur. C’est une convention, pas un verrou ; le langage vous fait confiance. Une @property est la manière habituelle d’exposer une vue sûre, en lecture seule (ou validée), d’une telle valeur interne.

Attributs de classe vs attributs d’instance

Voici le piège de la POO qui mord tout le monde exactement une fois. Un attribut affecté dans le corps de la classe (et non à l’intérieur de __init__) appartient à la classe elle-même et est partagé par toutes les instances. Pour une valeur immuable, c’est souvent sans conséquence, mais pour une valeur mutable — une liste, un dict — tous les objets finissent par partager le même objet, et les modifications fuient d’une instance à l’autre :

class Cart:
    items = []                          # BUG: one shared list on the CLASS, not per instance
    def add(self, product):
        self.items.append(product)

c1 = Cart()
c2 = Cart()
c1.add("apple")                         # only c1 got an apple...
print("c2.items:", c2.items)            # ...but c2 sees it too — same shared list
c2.items: ['apple']

c2.items vaut ['apple'] alors que nous n’avons ajouté qu’à c1 — les deux paniers pointent vers l’unique liste stockée sur la classe, si bien que l’ajout fait sur c1 est visible via c2. La correction consiste à donner à chaque objet sa propre liste en l’affectant dans __init__, où self est l’instance individuelle :

class Cart:
    def __init__(self):
        self.items = []                 # a NEW list per instance — no sharing
    def add(self, product):
        self.items.append(product)

c1 = Cart()
c2 = Cart()
c1.add("apple")
print("c1.items:", c1.items)
print("c2.items:", c2.items)            # independent now
c1.items: ['apple']
c2.items: []

Maintenant c1.items vaut ['apple'] et c2.items vaut [] — chaque panier possède une liste distincte parce que self.items = [] s’exécute à neuf pour chaque instance. La règle : les données partagées/constantes peuvent vivre dans le corps de la classe ; tout ce qui est propre à l’objet — et toujours une valeur mutable par défaut — a sa place dans __init__ sur self. Le même piège réapparaît avec @dataclass (juste après), où la correction est field(default_factory=list).

Les attributs de classe ne sont pas qu’un piège, cependant — une valeur délibérément partagée est un usage légitime. Un compteur au niveau de la classe, incrémenté dans __init__, suit combien d’instances existent, précisément parce qu’il est partagé :

class Counter:
    count = 0                           # shared on the class — deliberately

    def __init__(self):
        Counter.count += 1              # bump the shared class attribute on each creation

Counter()
Counter()
print(Counter.count)                    # how many instances were made
2

Counter.count vaut 2 après la création de deux instances — l’attribut de classe partagé est exactement ce que nous voulons ici. La leçon n’est pas « n’utilisez jamais d’attributs de classe », mais « sachez qu’ils sont partagés, et ne laissez jamais cette chose partagée être une valeur mutable par défaut propre à l’objet ».

@dataclass : supprimer le boilerplate

Beaucoup de classes n’existent que pour porter des données — quelques champs, plus l’évident __init__, un __repr__ et un __eq__ pour que deux valeurs égales se comparent comme égales. Écrire les trois à la main est fastidieux et source d’erreurs. Le décorateur @dataclass (du module dataclasses de la bibliothèque standard) les génère pour vous à partir d’une liste de champs typés :

from dataclasses import dataclass
import math

@dataclass
class Point:
    x: float
    y: float
    label: str = "origin"              # a field with a default

    def dist(self):                    # you still add your own methods
        return math.hypot(self.x, self.y)

p = Point(3, 4)
print(p)                               # auto __repr__
print(p.dist())                        # your own method
print(Point(3, 4) == Point(3, 4))      # auto __eq__ — compares by field values
Point(x=3, y=4, label='origin')
5.0
True

À partir de trois champs typés, @dataclass a écrit le __init__ (donc Point(3, 4) fonctionne d’emblée, label valant par défaut "origin"), un __repr__ lisible (afficher p donne Point(x=3, y=4, label='origin')) et un __eq__ qui compare par valeur — de sorte que Point(3, 4) == Point(3, 4) vaut True, là où deux objets de classe ordinaire écrits à la main auraient été inégaux par défaut (comparés par identité). Vous écrivez toujours vos propres méthodes : p.dist() renvoie 5.0 (l’hypoténuse d’un triangle 3-4). Pour une classe dont le rôle est de porter des données, c’est le choix moderne par défaut.

Le piège des attributs de classe vous suit ici : une valeur mutable par défaut (items: list = []) est partagée entre les instances, et Python lève carrément une erreur pour vous arrêter. La correction propre aux dataclasses est field(default_factory=list) — elle appelle list() à neuf pour chaque nouvelle instance :

from dataclasses import dataclass, field

@dataclass
class Basket:
    items: list = field(default_factory=list)   # a NEW list per instance

b1 = Basket()
b2 = Basket()
b1.items.append("apple")
print("b1.items:", b1.items)
print("b2.items:", b2.items)                     # independent, not shared
b1.items: ['apple']
b2.items: []

b1.items vaut ['apple'] et b2.items vaut []default_factory=list a donné à chaque panier sa propre liste, l’équivalent, côté dataclass, d’affecter self.items = [] dans __init__. Recourez à field(default_factory=…) pour toute valeur mutable par défaut d’une dataclass (listes, dicts, ensembles).

Quand avez-vous vraiment besoin d’une classe ?

Les classes sont un outil, pas un but — et en abuser est un problème tout aussi réel que ne pas assez y recourir. Le test honnête : recourez à une classe quand données et comportement voyagent ensemble (des valeurs toujours passées en groupe, plus les fonctions qui agissent dessus) ou quand vous avez besoin de plusieurs instances indépendantes portant chacune un état (beaucoup de comptes, beaucoup de paniers). Si vous n’avez ni l’un ni l’autre — une transformation ponctuelle, un sac de valeurs que vous ne faites que lire — une simple fonction, ou un dict / une @dataclass légère, est généralement plus clair qu’une classe complète avec des méthodes.

Vous avez… Recourez à…
Une seule transformation, sans état persistant une fonction
Un sac de valeurs liées que vous ne faites surtout que lire un dict ou une @dataclass
Des données et un comportement qui voyagent toujours ensemble une classe (des méthodes sur les données)
Plusieurs instances avec état (beaucoup de comptes, paniers, connexions) une classe
Une spécialisation d’un type existant (un seul niveau) une sous-classe (super()) — ou la composition

Un réflexe de sur-ingénierie voisin consiste à tout envelopper dans des objets alors que de simples fonctions se lisent mieux. Python est volontiers multi-paradigme : map et filter font, de manière fonctionnelle, ce que fait une compréhension, et pour la plupart du code, la compréhension est l’écriture la plus lisible :

nums = [1, 2, 3, 4, 5]

functional = list(map(lambda x: x * x, filter(lambda x: x % 2, nums)))
comprehension = [x * x for x in nums if x % 2]

print(functional)
print(comprehension)
print(functional == comprehension)
[1, 9, 25]
[1, 9, 25]
True

Les deux calculent les carrés des nombres impairs — [1, 9, 25] chacun — et ils sont égaux. Le filter(lambda x: x % 2, nums) conserve les valeurs impaires (un reste non nul est vrai) et map les met au carré ; la compréhension [x*x for x in nums if x % 2] dit la même chose en une ligne lisible. Ni l’une ni l’autre n’a eu besoin d’une classe. À retenir : utilisez la plus petite construction qui exprime clairement l’idée — une fonction, une compréhension, un dict, une dataclass ou une classe complète, à peu près dans cet ordre de recours.

Essayez en direct

Les blocs ci-dessus se sont exécutés au moment du build. Modifiez celui-ci et appuyez sur Run pour écrire votre propre classe — il s’exécute dans votre navigateur via Pyodide (aucune installation). Les classes et les dataclasses sont du pur Python, donc cela s’exécute instantanément, sans rien à télécharger.

🟢 Avec un agent IA

Vous fixez une classe qui a accumulé un enchevêtrement de méthodes, ou un paquet de valeurs que vous passez sans cesse en arguments séparés ? Collez votre code et demandez à Prova « est-ce que ça devrait être une classe, une dataclass ou juste une fonction ? » — ou « transforme la valeur mutable par défaut de cette dataclass en field(default_factory=…) ». Elle pèse si données et comportement voyagent réellement ensemble, réécrit le boilerplate (__init__, __repr__, __eq__ via @dataclass) et signale le piège de l’attribut mutable partagé avant qu’il ne morde. Parce que le runtime est juste là, vous exécutez la réécriture et confirmez qu’elle se comporte à l’identique — vous ne faites pas confiance au refactoring, vous le prouvez. The runtime is the judge. Demandez à Prova →

La même idée en R

Vous travaillez dans les deux langages ? Python a un modèle de classe principal — class, __init__, des méthodes sur l’objet. R possède plusieurs systèmes de POO qui coexistent : S3 (informel, celui du quotidien), S4 (formel, avec des slots déclarés et une validité), R5/Reference Classes (intégré, mutable) et R6 (un paquet populaire, mutable et le plus proche des classes de Python par l’esprit). La différence de fond est le style : Python fonctionne par passage de messages — vous appelez une méthode sur un objet (account.deposit(50)), et l’objet possède ses méthodes. Les S3/S4 de R reposent sur des fonctions génériques — vous appelez une fonction générique comme summary(x) ou predict(model), et R aiguille vers la bonne méthode selon la classe de l’argument. Ainsi, Python emballe le comportement à l’intérieur de la classe ; le R classique garde la fonction générique à l’extérieur et choisit la méthode selon le type de l’argument. R6 réduit l’écart quand vous voulez des objets à la Python, avec self. Voir le pilier Programmation pour le versant R.

Problèmes fréquents

Vous avez oublié self comme premier paramètre d’une méthode. Le premier paramètre de chaque méthode doit être self (l’instance) — def deposit(self, amount):, et non def deposit(amount):. Omettez-le et l’appel a.deposit(50) passe a comme premier paramètre déclaré, si bien que votre amount devient silencieusement l’objet compte, ou bien vous obtenez TypeError: deposit() takes 1 positional argument but 2 were given. La règle est mécanique : les méthodes d’instance commencent par self, toujours. (Vous y accédez dans le corps via self.balance ; vous ne le passez jamais au point d’appel.)

Un attribut de classe mutable (ou une valeur par défaut de dataclass) est partagé par toutes les instances. Affecter items = [] dans le corps de la classe — ou items: list = [] dans une @dataclass — crée une seule liste que toutes les instances partagent, si bien que le .append d’un objet fuit dans tous les autres. Corrigez-le en rendant l’état propre à chaque instance : affectez self.items = [] dans __init__, ou, dans une dataclass, utilisez field(default_factory=list). (Une dataclass lève d’ailleurs ValueError sur une valeur mutable par défaut nue pour vous arrêter ; une classe ordinaire se comporte mal en silence, ce qui est pire.)

Vous avez oublié super().__init__() dans une sous-classe, si bien que l’état de base manque. Quand une sous-classe définit son propre __init__, le __init__ du parent ne s’exécute pas automatiquement — vous devez appeler super().__init__(…). Omettez-le et les attributs que la classe de base était censée définir (ici owner, balance) ne sont jamais créés, si bien que la première méthode qui lit self.balance lève AttributeError. Si une sous-classe a besoin de la mise en place du parent, appelez d’abord super().__init__(…), puis ajoutez ses propres attributs.

Vous avez appelé une @property avec des parenthèses. Une @property s’obtient comme un attribut — c.area, et non c.area(). Ajouter les parenthèses tente d’appeler la valeur calculée : si area renvoyait un nombre, vous obtenez TypeError: 'float' object is not callable. Tout l’intérêt de @property est qu’elle se lit comme une donnée, alors supprimez les (). (À l’inverse, une méthode ordinaire, elle, a besoin des parenthèses — p.dist() — donc faites attention à celle que vous avez définie.)

Recourir à une chaîne d’héritage profonde plutôt qu’à la composition. Quand vous vous surprenez à sous-classer une sous-classe pour greffer « juste un comportement de plus », la conception dérive vers une hiérarchie fragile et difficile à suivre. Gardez l’héritage sur un seul niveau pour une véritable spécialisation « est-un » ; sinon, préférez la composition — donnez à une classe un attribut qui contient un autre objet et déléguez-lui le travail. « A-un » (une Order a un Cart) est généralement plus souple et plus facile à raisonner que « est-un » empilé sur plusieurs niveaux.

Questions fréquentes

self est l’instance sur laquelle la méthode a été appelée — l’objet précis, pas la classe. C’est le nom conventionnel du premier paramètre de toute méthode d’instance, et Python le passe automatiquement : quand vous écrivez a.deposit(50), Python appelle Account.deposit(a, 50), en liant self à a. Dans la méthode, vous utilisez self pour lire et définir les attributs de cet objet (self.balance += amount). Vous ne passez jamais self vous-même au point d’appel, et __init__(self, …) est l’endroit où vous attachez pour la première fois les attributs d’une instance. Le nom self n’est pas un mot-clé — c’est une convention forte que tout le monde suit ; en utiliser un autre fonctionne, mais déroute tout lecteur Python.

Un attribut de classe est affecté dans le corps de la classe et est partagé par toutes les instances — il en existe une seule copie, sur la classe. Un attribut d’instance est affecté sur self (généralement dans __init__) et est propre à chaque objet. Utilisez un attribut de classe pour des données vraiment partagées ou constantes (un taux par défaut, un compteur partagé) ; utilisez un attribut d’instance pour tout ce qui diffère d’un objet à l’autre. Le bug classique consiste à mettre une valeur mutable (une liste ou un dict) dans le corps de la classe : toutes les instances partagent et modifient alors le même objet. Pour l’état propre à chaque objet — et toujours pour les valeurs mutables par défaut — affectez-le dans __init__ (self.items = []), ou, dans une dataclass, utilisez field(default_factory=list).

Utilisez une @dataclass quand le rôle principal de la classe est de porter des données — quelques champs typés pour lesquels vous voulez que __init__, __repr__ et __eq__ soient écrits à votre place (et, en option, l’ordre, l’immuabilité avec frozen=True, et plus encore). Elle supprime le boilerplate et offre gratuitement une égalité fondée sur les valeurs, d’où son statut de choix moderne par défaut pour les objets « de type enregistrement », et vous pouvez toujours y ajouter vos propres méthodes. Recourez à une classe ordinaire quand vous avez besoin d’un contrôle total sur __init__ (mise en place complexe, champs calculés ou validés au-delà de field), quand l’objet se définit surtout par son comportement plutôt que par ses données, ou quand vous ciblez un Python dépourvu de dataclasses. Pour une valeur mutable par défaut dans une dataclass, n’oubliez pas field(default_factory=list).

Utilisez une classe quand données et comportement voyagent ensemble — des valeurs toujours passées en groupe, plus les fonctions qui agissent dessus — ou quand vous avez besoin de plusieurs instances indépendantes portant un état (beaucoup de comptes, de paniers ou de connexions, chacun avec ses propres données). Si vous n’avez ni l’un ni l’autre, une simple fonction (pour une transformation ponctuelle) ou un dict / une @dataclass légère (pour un sac de valeurs) est généralement plus clair qu’une classe complète. N’enveloppez pas tout dans des objets : Python est volontiers multi-paradigme, et la plus petite construction qui exprime l’idée — fonction, compréhension, dict, dataclass, classe — est presque toujours la bonne.

super() vous donne la classe parente, afin qu’une sous-classe puisse réutiliser l’implémentation du parent au lieu de la recopier. L’usage le plus courant est super().__init__(…) à l’intérieur du __init__ d’une sous-classe : il exécute l’initialiseur du parent pour que les attributs hérités (comme owner et balance) soient réellement définis, puis la sous-classe ajoute les siens. Si une sous-classe définit __init__ mais n’appelle jamais super().__init__(…), la mise en place de base est sautée et lire un attribut hérité plus tard lève AttributeError. super() pilote aussi la redéfinition de méthodes — appelez super().method(…) pour étendre le comportement du parent plutôt que le remplacer — et il gère correctement l’héritage multiple via l’ordre de résolution des méthodes de Python.

Testez vos connaissances

Assemblez toute la leçon sur l’exemple du compte.

Tâche 1. Écrivez une classe Account avec __init__(self, owner, balance=0), une méthode deposit(self, amount) qui ajoute au solde et le renvoie, une méthode withdraw(self, amount) qui retranche du solde et le renvoie, et un __repr__ qui affiche Account('Ada', 150). Créez-en une, déposez et retirez, puis affichez-la.

Tâche 2. Écrivez une sous-classe Savings(Account) qui ajoute un rate (par défaut 0.05) via super().__init__(…) et une méthode add_interest() qui ajoute balance * rate au solde. Vérifiez que isinstance(a_savings, Account) vaut True.

Tâche 3. Réécrivez Account comme une @dataclass avec les champs owner: str et balance: float = 0. Quelles trois méthodes @dataclass a-t-elle écrites pour vous, et laquelle fait que Account("Ada", 100) == Account("Ada", 100) vaut True ?

Pour la tâche 1, stockez self.owner/self.balance dans __init__ et renvoyez self.balance depuis chaque mutateur ; le __repr__ utilise une f-string avec !r sur le propriétaire : f"Account({self.owner!r}, {self.balance})". Pour la tâche 2, appelez super().__init__(owner, balance) d’abord, puis définissez self.rate = rate. Pour la tâche 3, décorez avec @dataclass et déclarez les deux champs comme des annotations typées — pas besoin de __init__.

from dataclasses import dataclass

# Task 1 --------------------------------------------------------------
class Account:
    def __init__(self, owner, balance=0):
        self.owner = owner
        self.balance = balance
    def deposit(self, amount):
        self.balance += amount
        return self.balance
    def withdraw(self, amount):
        self.balance -= amount
        return self.balance
    def __repr__(self):
        return f"Account({self.owner!r}, {self.balance})"

a = Account("Ada", 100)
a.deposit(50)          # 150
a.withdraw(20)         # 130
print(a)               # Account('Ada', 130)

# Task 2 --------------------------------------------------------------
class Savings(Account):
    def __init__(self, owner, balance=0, rate=0.05):
        super().__init__(owner, balance)   # set owner + balance via the parent
        self.rate = rate
    def add_interest(self):
        self.balance += self.balance * self.rate
        return self.balance

s = Savings("Bo", 1000)
print(s.add_interest())        # 1050.0
print(isinstance(s, Account))  # True

# Task 3 --------------------------------------------------------------
@dataclass
class Account:
    owner: str
    balance: float = 0

print(Account("Ada", 100) == Account("Ada", 100))   # True

a s’affiche comme Account('Ada', 130) (dépôt puis retrait), s.add_interest() renvoie 1050.0, et isinstance(s, Account) vaut True — un Savings est un Account. Tâche 3 : @dataclass a écrit __init__, __repr__ et __eq__ ; l’__eq__ auto-généré (qui compare par valeurs de champs) est ce qui fait que les deux Account de valeurs égales se comparent comme True.

Vérification rapide. Vous écrivez class Cart: items = [] et lui donnez une méthode add qui fait self.items.append(product). Vous créez deux paniers et ajoutez une pomme au premier. Qu’affiche le items du second panier, et pourquoi ?

Le items du second panier affiche ['apple'] lui aussi. Parce que items = [] est affecté dans le corps de la classe, c’est un unique attribut de classe partagé par toutes les instances — les deux paniers pointent vers la même liste, si bien que c1.add("apple") est visible via c2. La correction consiste à donner à chaque panier sa propre liste en l’affectant par instance dans __init__ (self.items = []), ou, dans une @dataclass, field(default_factory=list).

Conclusion

Vous savez désormais écrire vos propres classes de façon pratique. Une classe regroupe données et comportement ; __init__(self, …) met en place chaque instance et self est cette instance. Un __repr__ lisible rend chaque log et chaque vue de débogueur intelligible. L’héritage réutilise une classe de base — gardez-le sur un seul niveau, appelez super().__init__(…), et préférez la composition aux arbres profonds. @property expose une valeur calculée comme un attribut propre ; le piège attribut de classe vs attribut d’instance signifie que l’état mutable propre à chaque objet a sa place dans __init__ (ou field(default_factory=…)) ; et @dataclass écrit pour vous le boilerplate __init__/__repr__/__eq__. Par-dessus tout, recourez à la plus petite construction qui exprime l’idée — une fonction ou un dict suffit souvent, et une classe ne se justifie que lorsque données et comportement voyagent réellement ensemble. Cela achève la série Python Intermédiaire — vous disposez maintenant des itérateurs, des décorateurs, des gestionnaires de contexte, de la concurrence et de la POO dans votre boîte à outils.

Leçons connexes

Cette page vous a-t-elle été utile ?

Prouvez que vous savez le faire. Maîtrisez toute la série Python intermédiaire — suivez votre parcours, construisez des projets et obtenez un certificat.

Commencer gratuitement →

Passez à Pro — Prova illimité sur vos propres données et un certificat vérifiable qui atteste la compétence.

dès 15 $/mois facturé annuellement

Passer à Pro →

✓ Vous êtes Pro — continuez. The runtime is the judge.

Recevez les nouvelles leçons R & Python par e-mail

Pratique, reproductible, sans spam. Désinscription à tout moment.

Double opt-in. Nous ne partageons jamais votre e-mail.

Partager cette pageXLinkedInRedditHN

Réutilisation

Citation

BibTeX
@online{2026,
  author = {},
  title = {POO en Python : classes, self, héritage et dataclasses},
  date = {2026-07-18},
  url = {https://www.datanovia.com/learn/programming/python-intermediate/oop-essentials-python},
  langid = {fr}
}
Veuillez citer ce travail comme suit :
“POO en Python : classes, self, héritage et dataclasses.” 2026. July 18. https://www.datanovia.com/learn/programming/python-intermediate/oop-essentials-python.