Por defecto, Python asigna un diccionario __dict__ a cada instancia de una clase. Es flexible, sí. Pero cuando se tienen miles o millones de objetos en memoria al mismo tiempo, ese coste se acumula rápidamente. __slots__ elimina ese diccionario y reemplaza el almacenamiento por instancia con descriptores internos más compactos. Resultado típico: entre un 40 y un 60 por ciento menos de memoria por instancia.

Lo que hace Python sin slots

Sin ninguna declaración especial, cada instancia lleva su propio __dict__:

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

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

Este diccionario permite añadir atributos en tiempo de ejecución:

p.z = 3.0       # funciona
p.etiqueta = "A"   # también funciona

Esa flexibilidad tiene un precio. Un dict vacío en Python ocupa varios cientos de bytes (184 bytes en Python 3.12, 232 en Python 3.8), y cada instancia paga ese coste.

slots: eliminar el dict

Declarar __slots__ le indica a Python el conjunto exacto de atributos que puede tener una instancia. Python omite por completo la asignación del __dict__ y utiliza descriptores de slot en su lugar:

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

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

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

Intentar asignar un atributo no declarado lanza inmediatamente un AttributeError:

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

Medir la ganancia real

Aquí hay un benchmark concreto usando tracemalloc:

import tracemalloc


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


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

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


N = 100_000

tracemalloc.start()
sin_slots = [SinSlots(i, i) for i in range(N)]
_, pico_sin = tracemalloc.get_traced_memory()
tracemalloc.stop()

tracemalloc.start()
con_slots = [ConSlots(i, i) for i in range(N)]
_, pico_con = tracemalloc.get_traced_memory()
tracemalloc.stop()

print(f"Sin __slots__: {pico_sin / 1_000_000:.1f} MB")
print(f"Con __slots__: {pico_con / 1_000_000:.1f} MB")

Salida típica en Python 3.12:

Sin __slots__: 28.3 MB
Con __slots__:  9.6 MB

Alrededor de un 66 por ciento de reducción. El número exacto varía según la cantidad de atributos y su tipo.

Cuándo usar slots

Grandes cantidades de instancias en memoria simultáneamente. Casos clásicos: objetos de datos para simulaciones, nodos de grafos o árboles, eventos en un pipeline de procesamiento, registros cargados en masa desde una API o un fichero plano.

Prevenir la creación accidental de atributos. En value objects y DTOs, prohibir los atributos dinámicos elimina una clase de bugs sutil: una errata en un nombre de atributo que crea silenciosamente un atributo nuevo en lugar de modificar el que se pretendía.

Velocidad de acceso a atributos. El acceso mediante descriptor es marginalmente más rápido que la búsqueda en tabla hash a través de __dict__. La diferencia rara vez importa en una petición web típica, pero se aprecia en benchmarks ajustados o bucles internos críticos.

Las trampas que conviene conocer

Herencia: clases padre sin slots

Si una clase padre no tiene __slots__, sus instancias llevan un __dict__, y las subclases lo heredan aunque declaren sus propios slots:

class Base:
    pass  # sin __slots__ → tiene __dict__

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

h = Hijo()
h.sorpresa = "sigue funcionando"  # Base no tiene __slots__, sus instancias tienen __dict__ que Hijo hereda

Para que el beneficio de memoria se mantenga en toda la jerarquía, cada clase de la cadena debe declarar sus propios __slots__ sin heredar de una clase que tenga __dict__.

Incompatibilidades con frameworks

Varias bibliotecas esperan encontrar un __dict__ en las instancias: algunos ORMs, frameworks de serialización, e implementaciones de pickle o copia profunda según su versión. Verificar la compatibilidad antes de aplicar __slots__ a clases usadas con frameworks de terceros.

weakref también desaparece

Las instancias con __slots__ pierden el soporte para referencias débiles por defecto. Si se necesita, añadir "__weakref__" explícitamente a la lista:

class Nodo:
    __slots__ = ("valor", "siguiente", "__weakref__")

dataclasses y slots

Desde Python 3.10, @dataclass acepta slots=True directamente:

from dataclasses import dataclass


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

Esta es la forma más limpia de usar __slots__ en clases de datos modernas. Sin declaración manual, sin riesgo de que la lista de slots quede desincronizada con los campos.

Resumen

Sin slotsCon slots
Almacenamiento de atributos__dict__ por instanciaDescriptores internos
Añadir atributos dinámicosNo
Memoria por instanciaMayor40 a 60% menos
Velocidad de accesoLigeramente más lentoLigeramente más rápido
Compatibilidad con frameworksUniversalHay que verificar
Herencia multinivelSimpleRequiere disciplina

__slots__ no es la primera optimización a la que recurrir. Es una herramienta de precisión para situaciones donde la memoria ha sido medida y documentada como un problema real. Cientos de objetos en una aplicación web típica: impacto despreciable. Millones de objetos en memoria durante una ejecución de pipeline o una simulación: uno de los palancas más directas disponibles en Python puro.