
Une métaclasse est la classe d'une classe : elle contrôle comment une classe est construite, exactement comme une classe contrôle comment ses instances sont construites. En Python, tout objet a un type, et le type d'une classe est presque toujours type. Une métaclasse personnalise cette fabrique pour intervenir au moment où la classe naît : enregistrer automatiquement des sous-classes, valider leur structure ou imposer une API. Ce guide couvre type comme métaclasse universelle, la création dynamique de classes, l'écriture d'une métaclasse avec __new__/__init__/__call__, un registre de classes fonctionnel, et surtout l'alternative moderne __init_subclass__, souvent préférable.
Il s'adresse aux développeurs Python confirmés qui rencontrent les métaclasses dans un framework (Django, SQLAlchemy, les ORM, abc) et veulent comprendre le mécanisme sans en abuser. Les exemples ont été exécutés avec Python 3.12.
Ce que vous allez apprendre
Section intitulée « Ce que vous allez apprendre »- Comprendre que
typeest la métaclasse de toutes les classes. - Créer une classe dynamiquement avec
type(nom, bases, dict). - Écrire une métaclasse via
__new__,__init__et__call__. - Bâtir un registre de classes qui s'alimente tout seul.
- Préférer
__init_subclass__et__set_name__quand ils suffisent. - Savoir quand ne pas utiliser de métaclasse.
type, la métaclasse de toutes les classes
Section intitulée « type, la métaclasse de toutes les classes »En Python, tout est objet, y compris les classes. Un objet possède un type, accessible avec la fonction type(). Le type d'une instance est sa classe ; le type d'une classe est sa métaclasse. Dans la quasi-totalité des cas, cette métaclasse est type elle-même.
class Serveur: pass
s = Serveur()print(type(s)) # la classe de l'instanceprint(type(Serveur)) # la metaclasse de la classeprint(type(int)) # meme les types natifsprint(isinstance(Serveur, type))L'exécution montre la hiérarchie complète des types :
<class '__main__.Serveur'><class 'type'><class 'type'>TrueL'instance s a pour type Serveur, mais Serveur a pour type type. Même int est une instance de type. Autrement dit, type fabrique les classes comme une classe fabrique ses instances. Une métaclasse personnalisée n'est rien d'autre qu'une sous-classe de type que l'on branche à la place du comportement par défaut.
Créer une classe dynamiquement avec type()
Section intitulée « Créer une classe dynamiquement avec type() »type a deux visages. Appelée avec un seul argument, elle inspecte le type d'un objet. Appelée avec trois arguments, elle fabrique une nouvelle classe à la volée. La signature est type(nom, bases, dict) : le nom de la classe, un tuple de classes parentes, et un dictionnaire d'attributs et méthodes.
def demarrer(self): return f"{self.nom} demarre"
Service = type( "Service", (), {"nom": "nginx", "demarrer": demarrer},)
s = Service()print(type(s))print(s.nom)print(s.demarrer())Le résultat est une classe parfaitement ordinaire :
<class '__main__.Service'>nginxnginx demarreCette classe Service est strictement équivalente à une définition écrite avec le mot-clé class. La syntaxe class est en réalité du sucre syntaxique : Python collecte le nom, les bases et le corps, puis appelle la métaclasse pour produire l'objet classe. Comprendre cette équivalence est la clé mentale des métaclasses : une métaclasse intercepte précisément cet appel de fabrication.
Écrire une métaclasse : new, init, call
Section intitulée « Écrire une métaclasse : new, init, call »Une métaclasse se déclare en héritant de type, puis se branche sur une classe avec l'argument metaclass=. Elle expose trois points d'entrée. __new__ construit l'objet classe (il reçoit le nom, les bases, l'espace de noms). __init__ l'initialise juste après. __call__ s'exécute chaque fois qu'on instancie la classe, c'est-à-dire à chaque MaClasse(...).
class Trace(type): def __new__(mcs, nom, bases, espace_noms): print(f"__new__ appele pour {nom}") return super().__new__(mcs, nom, bases, espace_noms)
def __init__(cls, nom, bases, espace_noms): print(f"__init__ appele pour {nom}") super().__init__(nom, bases, espace_noms)
def __call__(cls, *args, **kwargs): print(f"__call__ : on instancie {cls.__name__}") return super().__call__(*args, **kwargs)
class Sonde(metaclass=Trace): def __init__(self, cible): self.cible = cible
print("--- creation terminee, on instancie ---")s = Sonde("db-prod")print(s.cible)La sortie révèle quand chaque méthode se déclenche :
__new__ appele pour Sonde__init__ appele pour Sonde--- creation terminee, on instancie ---__call__ : on instancie Sondedb-prodPoint capital : __new__ et __init__ de la métaclasse tournent une seule fois, à la définition de Sonde, avant même que le programme n'atteigne la ligne d'instanciation. __call__ tourne ensuite à chaque création d'instance. C'est cette distinction qui donne aux métaclasses leur pouvoir : agir au moment de la naissance de la classe, pas de ses objets.
Cas d'usage 1 : un registre de classes automatique
Section intitulée « Cas d'usage 1 : un registre de classes automatique »Le cas d'usage le plus légitime d'une métaclasse est le registre automatique. L'objectif : dès qu'une sous-classe est définie, elle s'inscrit dans un annuaire central sans qu'on ait à l'y ajouter à la main. C'est le mécanisme derrière les systèmes de plugins, les fabriques de parsers ou les backends de sérialisation.
class RegistreParsers(type): registre = {}
def __new__(mcs, nom, bases, espace_noms): cls = super().__new__(mcs, nom, bases, espace_noms) format_gere = espace_noms.get("format") if format_gere is not None: mcs.registre[format_gere] = cls return cls
class Parser(metaclass=RegistreParsers): format = None
class ParserJSON(Parser): format = "json" def lire(self, texte): return f"JSON: {texte}"
class ParserYAML(Parser): format = "yaml" def lire(self, texte): return f"YAML: {texte}"
print(RegistreParsers.registre)
def parser_pour(format_demande): cls = RegistreParsers.registre[format_demande] return cls()
p = parser_pour("yaml")print(p.lire("cle: valeur"))Chaque sous-classe s'inscrit d'elle-même au moment de sa définition :
{'json': <class '__main__.ParserJSON'>, 'yaml': <class '__main__.ParserYAML'>}YAML: cle: valeurLe bénéfice est réel : ajouter un nouveau format ne demande aucune modification du code de dispatch. On écrit une nouvelle sous-classe avec son attribut format, et la fonction parser_pour la trouve immédiatement. La classe de base Parser porte format = None, ce qui l'exclut naturellement du registre. Ce découplage entre extension et cœur applicatif est l'argument massue en faveur des registres.
Cas d'usage 2 : valider et imposer une API
Section intitulée « Cas d'usage 2 : valider et imposer une API »Une métaclasse peut aussi refuser une classe mal formée. Comme __new__ s'exécute à la définition, on y vérifie que chaque sous-classe déclare bien les attributs ou méthodes attendus, et on lève une TypeError sinon. L'erreur remonte immédiatement, au chargement du module, plutôt que des heures plus tard à l'exécution.
class ExigeConfig(type): def __new__(mcs, nom, bases, espace_noms): if bases: # on ignore la classe de base for attribut in ("nom_service", "port"): if attribut not in espace_noms: raise TypeError( f"{nom} doit definir l'attribut '{attribut}'" ) return super().__new__(mcs, nom, bases, espace_noms)
class Composant(metaclass=ExigeConfig): pass
class ComposantWeb(Composant): nom_service = "nginx" port = 443
print("ComposantWeb valide")
try: class ComposantCasse(Composant): nom_service = "redis" # port manquantexcept TypeError as e: print(f"Erreur attrapee : {e}")La classe incomplète est rejetée à sa définition :
ComposantWeb valideErreur attrapee : ComposantCasse doit definir l'attribut 'port'Le test if bases: évite d'appliquer la contrainte à la classe de base Composant, qui n'a par nature aucune configuration. Ce type de garde-fou structurel garantit qu'aucun développeur ne pourra livrer un composant sans son port ou son nom_service. C'est proche de ce que fait le module standard abc avec les méthodes abstraites, qui repose lui-même sur une métaclasse (ABCMeta).
L'alternative moderne : init_subclass
Section intitulée « L'alternative moderne : init_subclass »Depuis Python 3.6 (PEP 487), la majorité des cas ci-dessus n'ont plus besoin de métaclasse. La méthode spéciale __init_subclass__ est appelée automatiquement sur la classe parente chaque fois qu'une sous-classe est définie. Elle couvre le registre et une bonne part de la validation, avec une syntaxe bien plus accessible et sans les pièges de l'héritage de métaclasses.
class Plugin: registre = {}
def __init_subclass__(cls, /, cle=None, **kwargs): super().__init_subclass__(**kwargs) if cle is not None: Plugin.registre[cle] = cls
class PluginSlack(Plugin, cle="slack"): def notifier(self, message): return f"Slack: {message}"
class PluginEmail(Plugin, cle="email"): def notifier(self, message): return f"Email: {message}"
print(Plugin.registre)notif = Plugin.registre["slack"]()print(notif.notifier("deploiement OK"))Le registre se remplit exactement comme avec la métaclasse :
{'slack': <class '__main__.PluginSlack'>, 'email': <class '__main__.PluginEmail'>}Slack: deploiement OKL'écriture est nettement plus directe. Remarquez que __init_subclass__ est implicitement une méthode de classe et qu'elle accepte des arguments par mot-clé passés dans la définition de sous-classe (cle="slack"). L'appel à super().__init_subclass__(**kwargs) reste obligatoire pour ne pas casser la chaîne d'héritage. Pour un registre ou une validation simple, c'est le premier réflexe à avoir avant même de penser aux métaclasses.
Nommer les attributs avec set_name
Section intitulée « Nommer les attributs avec set_name »Toujours issue de la PEP 487, la méthode __set_name__ résout un problème classique des descripteurs : un attribut de classe qui doit connaître son propre nom. Python appelle __set_name__(self, owner, name) sur chaque objet assigné dans le corps d'une classe, en lui transmettant la classe propriétaire et le nom sous lequel il a été défini. Plus besoin de métaclasse pour inspecter l'espace de noms.
class Champ: def __set_name__(self, owner, name): self.nom_prive = f"_{name}" print(f"__set_name__ : le champ s'appelle '{name}'")
def __get__(self, instance, owner=None): if instance is None: return self return getattr(instance, self.nom_prive, None)
def __set__(self, instance, valeur): setattr(instance, self.nom_prive, valeur)
class Configuration: hote = Champ() port = Champ()
c = Configuration()c.hote = "db-prod"c.port = 5432print(c.hote, c.port)Chaque champ apprend son nom automatiquement :
__set_name__ : le champ s'appelle 'hote'__set_name__ : le champ s'appelle 'port'db-prod 5432C'est exactement le mécanisme utilisé par les ORM modernes et les dataclasses pour lier un descripteur à son nom de colonne ou de champ. Avant la PEP 487, obtenir ce comportement imposait une métaclasse qui parcourait l'espace de noms. Aujourd'hui, __set_name__ le fait de façon native et locale, sans machinerie globale.
Le piège du conflit de métaclasses
Section intitulée « Le piège du conflit de métaclasses »Le principal danger opérationnel des métaclasses apparaît lors de l'héritage multiple. Python impose que la métaclasse d'une classe dérivée soit un sous-type des métaclasses de toutes ses bases. Deux hiérarchies bâties sur des métaclasses incompatibles ne peuvent pas fusionner, et l'erreur est souvent obscure pour qui n'en connaît pas la cause.
class MetaA(type): pass
class MetaB(type): pass
class A(metaclass=MetaA): pass
class B(metaclass=MetaB): pass
try: class C(A, B): passexcept TypeError as e: print(f"TypeError : {e}")Python refuse catégoriquement la combinaison :
TypeError : metaclass conflict: the metaclass of a derived class must be a (non-strict) subclass of the metaclasses of all its basesCe conflit de métaclasses est une plaie récurrente quand on mélange plusieurs bibliothèques qui utilisent chacune leur propre métaclasse. La seule sortie propre consiste à créer une métaclasse commune qui hérite des deux, ce qui alourdit encore le code. C'est l'une des raisons majeures pour lesquelles __init_subclass__, qui ne touche pas au type de la classe, ne provoque jamais ce genre de conflit.
Quand ne pas utiliser de métaclasse
Section intitulée « Quand ne pas utiliser de métaclasse »Tim Peters, auteur du Zen de Python, résume l'essentiel : « les métaclasses sont une magie plus profonde que ce dont 99 % des utilisateurs devraient jamais se soucier. Si vous vous demandez si vous en avez besoin, c'est que non. » Cette phrase n'est pas un mépris, c'est un avertissement de coût. Une métaclasse rend le code plus difficile à lire, à déboguer, à combiner et à typer.
Avant d'écrire une métaclasse, épuisez les alternatives dans cet ordre. Un simple décorateur de classe modifie une classe après sa création et suffit pour la plupart des transformations. __init_subclass__ couvre le registre et la validation des sous-classes. __set_name__ gère les descripteurs qui ont besoin de leur nom. Les dataclasses génèrent le code répétitif d'__init__ et de __repr__. Ces outils sont plus prévisibles et ne cassent pas l'héritage multiple.
La métaclasse ne se justifie que dans une poignée de situations : construire un framework dont les classes doivent partager un comportement structurel imposé, intercepter l'instanciation de toutes les classes d'une hiérarchie via __call__, ou manipuler l'espace de noms avant que la classe existe. Si votre besoin ne tombe pas exactement dans ces cases, choisissez l'outil plus simple : votre équipe vous remerciera à la relecture.
Contrôle de connaissances
Section intitulée « Contrôle de connaissances »Vérifiez que l'essentiel de ce guide est acquis. Les questions portent uniquement sur ce qui vient d'être expliqué ici.
Contrôle de connaissances
Validez vos connaissances avec ce quiz interactif
Informations
- Le chronomètre démarre au clic sur Démarrer
- Questions à choix multiples, vrai/faux et réponses courtes
- Vous pouvez naviguer entre les questions
- Les résultats détaillés sont affichés à la fin
Lance le quiz et démarre le chronomètre
Vérification
(0/0)Profil de compétences
Quoi faire maintenant
Ressources pour progresser
Des indices pour retenter votre chance ?
Nouveau quiz complet avec des questions aléatoires
Retravailler uniquement les questions ratées
Retour à la liste des certifications
À retenir
Section intitulée « À retenir »- La métaclasse d'une classe est sa fabrique ; par défaut c'est
type. type(nom, bases, dict)crée une classe dynamiquement ; le mot-cléclassn'est que du sucre syntaxique pour cet appel.- Une métaclasse hérite de
type:__new__/__init__tournent à la définition de la classe,__call__à chaque instanciation. - Le registre automatique de sous-classes est le cas d'usage le plus légitime.
- Une métaclasse peut valider la structure d'une classe et lever
TypeErrorau chargement. __init_subclass__(PEP 487) remplace la métaclasse pour registre et validation, sans conflit d'héritage.__set_name__donne son nom à un descripteur sans métaclasse.- L'héritage multiple de métaclasses incompatibles provoque un
metaclass conflict. - Règle d'or : si vous vous demandez s'il vous faut une métaclasse, c'est que non.
Pour aller plus loin
Section intitulée « Pour aller plus loin »- POO avancée en Python : classes abstraites
abc, MRO et mixins, le terrain naturel des métaclasses. - Les décorateurs en Python : l'alternative la plus fréquente à une métaclasse pour transformer une classe.
- Les dataclasses en Python : générer le code répétitif sans magie de métaclasse.
- Type hints en Python : annoter et vérifier un code qui, justement, supporte mal les classes dynamiques.
- Les design patterns : le registre automatique d'implémentations, un cas d'école où une métaclasse rend service.
- Les tests avancés : verrouiller par des tests le code généré dynamiquement, plus difficile à couvrir qu'un code statique.