En Python 3, super() n’est pas un simple raccourci pour appeler la classe mère. C’est un mécanisme de délégation qui suit l’ordre de résolution des méthodes, ce qui le rend beaucoup plus intéressant dès qu’une hiérarchie de classes doit rester propre, extensible et facile à faire évoluer. Je vais montrer quand l’utiliser, comment l’écrire sans piège, et pourquoi il devient vraiment utile dès qu’on quitte l’héritage le plus simple.
L’essentiel à retenir sur super() en Python 3
- super() appelle la prochaine méthode dans le MRO, pas forcément le parent direct.
- En héritage simple, il rend le code plus robuste face aux changements de base.
- En héritage multiple, il permet des appels coopératifs sans doubler le travail.
- Les classes chaînées doivent garder des signatures compatibles ou accepter
*argset**kwargs. - Les erreurs les plus fréquentes viennent des appels directs au parent, des fonctions imbriquées et des mixins qui n’appellent pas eux-mêmes
super().
Ce que fait vraiment super()
Je préfère penser à super() comme à un relais, pas comme à un appel vers un parent nommé. En pratique, il interroge le MRO (Method Resolution Order), c’est-à-dire l’ordre dans lequel Python cherche les méthodes lorsqu’une classe hérite d’autres classes.
La documentation officielle de Python le dit clairement: super() ne se contente pas de viser la classe parente immédiate. Il repart du point courant dans le MRO et appelle la classe suivante. C’est une nuance importante, parce qu’elle explique pourquoi le mécanisme reste fiable même si la hiérarchie bouge.
| Point comparé | Appel direct du parent | super() |
|---|---|---|
| Cible | Classe explicitement nommée | Classe suivante dans le MRO |
| Robustesse au refactoring | Faible | Élevée |
| Héritage multiple | Risque de contourner la chaîne | Adapté aux appels coopératifs |
| Lisibilité de l’intention | Très explicite, mais rigide | Plus abstrait, mais plus souple |
Autrement dit, quand je veux écrire du code qui supporte les évolutions futures, je pense moins en termes de “parent” qu’en termes de “prochaine étape dans la chaîne”. Cette différence devient encore plus visible dès qu’on regarde un héritage simple bien écrit.
L’utiliser dans un héritage simple sans fragiliser le code
Dans une hiérarchie simple, super() sert souvent à chaîner un __init__ ou une méthode surchargée sans figer le nom de la classe de base. C’est souvent là que les débutants comprennent mal le mécanisme, alors qu’il est déjà utile à ce stade.
class Compte:
def __init__(self, titulaire):
self.titulaire = titulaire
def afficher(self):
return f"Compte de {self.titulaire}"
class ComptePremium(Compte):
def __init__(self, titulaire, plafond):
super().__init__(titulaire)
self.plafond = plafond
def afficher(self):
base = super().afficher()
return f"{base} avec un plafond de {self.plafond} €"Ici, ComptePremium ne dépend pas du nom exact de la classe parente. Si je renomme Compte ou si j’insère plus tard une classe intermédiaire, le chaînage reste cohérent tant que la hiérarchie respecte la logique de délégation. C’est précisément ce que je recherche dans un code de backend qui doit rester maintenable.
La différence avec un appel direct est nette: Compte.__init__(self, titulaire) fonctionne aujourd’hui, mais il fige la relation. Je l’évite dès qu’une classe peut être reprise, enrichie ou combinée avec d’autres composants.
Dans un projet réel, cette souplesse compte vite. Une classe simple n’est pas toujours simple longtemps, et c’est là que super() prend de la valeur.
Le rendre coopératif en héritage multiple

Le vrai terrain de jeu de super(), c’est l’héritage multiple. Python a été pensé pour gérer des hiérarchies où plusieurs classes participent à un même comportement, à condition que chacune joue le jeu du chaînage.
Le modèle à retenir est celui de l’héritage coopératif: chaque classe fait sa part du travail, puis transmet proprement la suite avec super(). Si une seule classe casse la chaîne, tout l’édifice devient plus fragile.
class BaseService:
def save(self, data):
print("enregistrement final")
return data
class LoggingMixin:
def save(self, data):
print("journalisation")
return super().save(data)
class ValidationMixin:
def save(self, data):
print("validation")
return super().save(data)
class UserService(ValidationMixin, LoggingMixin, BaseService):
passQuand j’appelle UserService().save({}), Python suit le MRO de gauche à droite, sans repasser deux fois sur la même classe. Le résultat est logique: d’abord la validation, ensuite la journalisation, puis l’enregistrement final.
Le point décisif ici, c’est que les méthodes doivent rester compatibles entre elles. Si une classe attend des paramètres spécifiques et qu’une autre ne les relaie pas correctement, la chaîne casse. Dans les mixins, je recommande presque toujours une signature tolérante, souvent avec *args et **kwargs, surtout sur les méthodes d’initialisation.
class AuditMixin:
def __init__(self, *args, **kwargs):
self.audit_enabled = kwargs.pop("audit_enabled", False)
super().__init__(*args, **kwargs)Je vois souvent des équipes découvrir ce principe après coup, quand l’arborescence des classes devient plus riche. À ce moment-là, le MRO n’est plus un détail théorique: il devient le contrat qui permet à plusieurs briques de coopérer sans se marcher dessus.
Les formes d’appel à connaître selon le contexte
super() apparaît sous plusieurs formes, mais je conseille de privilégier la version sans argument dans les méthodes ordinaires. Elle est plus lisible, et Python déduit la classe courante ainsi que l’objet ou la classe concernée.
| Contexte | Forme courante | Ce qu’il faut retenir |
|---|---|---|
| Méthode d’instance | super().methode() |
La forme standard, la plus claire |
__init__ |
super().__init__(...) |
Indispensable pour les classes coopératives |
@classmethod |
super().methode() |
Python relie correctement cls à la chaîne |
| Fonction imbriquée | À éviter | Le zéro-argument ne se comporte pas comme on l’attend |
Un cas utile, mais moins fréquent, concerne les classmethod. Par exemple, une fabrique de classe peut déléguer une partie du travail à la méthode suivante dans le MRO, ce qui reste cohérent tant que la logique reste orientée classe et non instance.
class Document:
@classmethod
def from_config(cls, config):
return cls(config["title"])
class Report(Document):
@classmethod
def from_config(cls, config):
config = {**config, "title": config["title"].strip()}
return super().from_config(config)Je garde aussi une règle simple en tête: si j’ai besoin de forcer un comportement très précis vers une base connue, l’appel explicite peut se défendre. Mais dès que je veux une architecture extensible, je reviens à super(), parce que c’est lui qui laisse la chaîne respirer.
Les erreurs qui reviennent le plus souvent
Quand super() “ne marche pas”, le problème vient rarement de la fonction elle-même. Il vient plus souvent d’un contrat mal respecté dans la hiérarchie.
- Appeler la classe parente directement dans une seule classe casse souvent la coopération avec les autres mixins.
-
Oublier de relayer les arguments avec
*argset**kwargsprovoque vite des erreurs de signature. -
Utiliser
super()dans une fonction imbriquée peut produire un comportement inattendu, parce que la forme zéro argument dépend du contexte de méthode immédiat. -
Supposer que
super()vise toujours le parent conduit à des diagnostics faux dès qu’il y a plusieurs bases. -
Ne pas appeler
super()dans un maillon de la chaîne stoppe la propagation pour tout ce qui suit.
Le test que je fais presque systématiquement dans une hiérarchie complexe est simple: j’inspecte le MRO avec MaClasse.__mro__. Ce n’est pas un réflexe de curieux, c’est un contrôle de santé. Si l’ordre obtenu ne correspond pas à ce que j’essaie d’exprimer, je corrige la structure avant de continuer.
Il y a aussi une limite pratique à connaître: tous les objets hérités d’un ancien code ou d’une bibliothèque tierce ne sont pas forcément pensés pour des appels coopératifs. Dans ce cas, je fais un arbitrage pragmatique entre compatibilité, lisibilité et maintenabilité. Forcer super() partout n’est pas une religion, mais dans du code moderne en Python 3, c’est très souvent le meilleur point d’équilibre.
Ce que j’applique en pratique pour écrire des classes faciles à faire évoluer
Quand je conçois une classe qui a vocation à vivre dans une architecture plus large, j’adopte une règle simple: je relaye plutôt que je contourne. Cela veut dire que j’utilise super() dans les méthodes surchargées, que je garde des signatures compatibles, et que je vérifie le MRO dès que plusieurs classes interviennent dans le même flux.
Je réserve l’appel direct à une base à des cas très ciblés, souvent hérités d’un code ancien ou d’une contrainte d’intégration. Dans tous les autres cas, super() me donne un code plus lisible, plus stable et plus facile à étendre sans casser les classes voisines. Si je devais résumer la bonne pratique en une phrase, ce serait celle-ci: ne pense pas “parent”, pense “prochaine étape de la chaîne”.
C’est exactement ce qui fait la force de super() en Python 3: moins de dépendance au nom des classes, plus de coopération entre les composants, et une hiérarchie qui supporte mieux les évolutions réelles d’un projet backend.