Python crée par défaut un dictionnaire __dict__ pour chaque instance de classe. C’est flexible, pratique, mais coûteux en mémoire quand on instancie des milliers ou des millions d’objets. __slots__ est le mécanisme qui supprime ce dictionnaire et stocke les attributs dans une structure compacte. Gain typique : 40 à 60 % de mémoire en moins par instance.

Ce que fait Python sans slots

Sans rien déclarer, chaque instance porte son propre __dict__ :

class Point:
    def __init__(self, x: float, y: float) -> None:
        self.x = x
        self.y = y

p = Point(1.0, 2.0)
print(p.__dict__)  # {'x': 1.0, 'y': 2.0}

Ce dictionnaire permet d’ajouter des attributs à la volée :

p.z = 3.0       # fonctionne
p.label = "A"   # aussi

C’est la flexibilité par défaut de Python. Elle a un coût : un dict Python vide pèse plusieurs centaines d’octets (184 octets sur Python 3.12, 232 sur Python 3.8), et ce coût s’applique à chaque instance.

slots : supprimer le dict

En déclarant __slots__, on indique à Python la liste exacte des attributs qu’une instance peut avoir. Python ne crée plus de __dict__ et utilise des descripteurs internes à la place :

class Point:
    __slots__ = ("x", "y")

    def __init__(self, x: float, y: float) -> None:
        self.x = x
        self.y = y

p = Point(1.0, 2.0)
print(hasattr(p, "__dict__"))  # False

Tenter d’ajouter un attribut non déclaré lève immédiatement une AttributeError :

p.z = 3.0  # AttributeError: 'Point' object has no attribute 'z'

Mesurer le gain réel

Voici comment comparer concrètement avec tracemalloc :

import tracemalloc


class SansSlots:
    def __init__(self, x: float, y: float) -> None:
        self.x = x
        self.y = y


class AvecSlots:
    __slots__ = ("x", "y")

    def __init__(self, x: float, y: float) -> None:
        self.x = x
        self.y = y


N = 100_000

tracemalloc.start()
objets_sans = [SansSlots(i, i) for i in range(N)]
_, peak_sans = tracemalloc.get_traced_memory()
tracemalloc.stop()

tracemalloc.start()
objets_avec = [AvecSlots(i, i) for i in range(N)]
_, peak_avec = tracemalloc.get_traced_memory()
tracemalloc.stop()

print(f"Sans __slots__ : {peak_sans / 1_000_000:.1f} MB")
print(f"Avec __slots__ : {peak_avec / 1_000_000:.1f} MB")

Résultat typique sur Python 3.12 :

Sans __slots__ : 28.3 MB
Avec __slots__ :  9.6 MB

Soit une réduction de l’ordre de 66 %. Le gain exact dépend du nombre d’attributs et de leur type.

Quand utiliser slots

Beaucoup d’instances en mémoire simultanément. Les cas classiques : objets de données pour des simulations, nœuds d’arbre ou de graphe, événements dans un pipeline de traitement, enregistrements chargés en masse depuis une API.

Empêcher l’ajout accidentel d’attributs. Dans les classes de valeur ou les DTOs, interdire les attributs dynamiques évite une catégorie de bugs discrets (faute de frappe sur un nom d’attribut qui crée silencieusement un nouvel attribut au lieu de modifier l’existant).

Accélération de l’accès aux attributs. L’accès via descripteur est légèrement plus rapide que via __dict__. La différence est marginale dans la plupart des cas, mais elle peut s’observer sur des benchmarks serrés.

Les pièges à connaître

Héritage : attention aux classes parentes sans slots

Si une classe parente n’a pas de __slots__, ses instances ont un __dict__, et les sous-classes en héritent, même si elles déclarent leurs propres __slots__ :

class Base:
    pass  # pas de __slots__ → a un __dict__

class Enfant(Base):
    __slots__ = ("x",)

e = Enfant()
e.surprise = "je passe quand même"  # fonctionne : Base n'a pas de __slots__, ses instances ont un __dict__ qu'Enfant hérite

Pour un vrai bénéfice mémoire sur toute la hiérarchie, chaque classe doit déclarer ses propres __slots__ et ne pas hériter d’une classe avec __dict__.

Incompatibilité avec certains outils

Plusieurs bibliothèques s’attendent à trouver un __dict__ sur les instances : certains ORMs, des frameworks de sérialisation, des outils de copie profonde ou de pickling selon leur version. Avant d’appliquer __slots__ à une classe utilisée avec des frameworks tiers, tester la compatibilité.

__weakref__ disparaît aussi

Par défaut, les instances avec __slots__ ne supportent plus les références faibles (weakref). Si c’est nécessaire, ajouter explicitement "__weakref__" dans la liste :

class Noeud:
    __slots__ = ("valeur", "suivant", "__weakref__")

dataclasses et slots

Depuis Python 3.10, @dataclass accepte slots=True directement :

from dataclasses import dataclass


@dataclass(slots=True)
class Coordonnee:
    x: float
    y: float
    z: float

C’est la façon la plus propre d’utiliser __slots__ sur des classes de données modernes. Pas de déclaration manuelle, pas de désynchronisation possible entre __slots__ et les champs.

Récapitulatif

Sans slotsAvec slots
Stockage des attributs__dict__ par instanceDescripteurs internes
Ajout dynamique d’attributsOuiNon
Mémoire par instancePlus élevée40 à 60 % moins
Accès aux attributsLégèrement plus lentLégèrement plus rapide
Compatibilité frameworksUniverselleÀ vérifier
Héritage multi-niveauxSimpleNécessite une discipline

__slots__ n’est pas la première optimisation à appliquer. C’est un outil de précision pour les situations où la mémoire est mesurée et documentée comme un problème réel. Si on instancie des centaines d’objets dans une application web classique, l’impact est négligeable. Si on traite des millions d’objets en mémoire dans un pipeline ou une simulation, c’est l’un des leviers les plus directs disponibles en Python pur.