Las dataclasses de Python generan automáticamente __init__, __repr__ y __eq__ a partir de las anotaciones de tipo. En cuanto un atributo necesita un valor por defecto mutable (lista, diccionario, conjunto), nos encontramos con un problema fundamental del lenguaje. field(default_factory=...) es la solución, y entender por qué es necesario cambia la forma en que razonamos sobre la inicialización en Python.
La trampa de los valores mutables por defecto
En Python, los valores por defecto de los parámetros de función se evalúan una sola vez en el momento de definición de la función, no en cada llamada. Esto es una propiedad del lenguaje, no un bug.
def agregar(valor, lista=[]):
lista.append(valor)
return lista
print(agregar(1)) # [1]
print(agregar(2)) # [1, 2] ← se reutiliza la misma lista
Las dataclasses rechazan explícitamente este patrón:
from dataclasses import dataclass
@dataclass
class Carrito:
articulos: list = []
# ValueError: mutable default <class 'list'> for field articulos is not allowed: use default_factory
Python lanza un ValueError en el momento de definición de la clase, lo cual es más seguro que un bug silencioso. El mensaje indica incluso la solución: use default_factory.
Qué ocurriría sin la protección
Si Python permitiera valores mutables por defecto en las dataclasses, todas las instancias compartirían el mismo objeto en memoria. El comportamiento equivalente con una clase normal lo deja claro:
class Carrito:
articulos = [] # atributo de clase, compartido entre todas las instancias
a = Carrito()
b = Carrito()
a.articulos.append("manzana")
print(b.articulos) # ['manzana'] — b está contaminado
print(a.articulos is b.articulos) # True
Es este comportamiento el que @dataclass se niega a introducir silenciosamente.
La solución: default_factory
field(default_factory=...) recibe un callable sin argumentos y lo llama en cada creación de instancia:
from dataclasses import dataclass, field
@dataclass
class Carrito:
articulos: list = field(default_factory=list)
a = Carrito()
b = Carrito()
a.articulos.append("manzana")
print(b.articulos) # []
print(a.articulos is b.articulos) # False
list aquí es la función builtin, no una lista vacía. En cada Carrito(), Python llama a list() para crear una nueva lista independiente.
Qué genera @dataclass internamente
@dataclass genera el __init__ mediante exec() en el momento de definición de la clase. Para un campo con default_factory, la factory se almacena en el objeto Field, accesible a través de dataclasses.fields():
import dataclasses
from dataclasses import dataclass, field
@dataclass
class Carrito:
articulos: list = field(default_factory=list)
campo = dataclasses.fields(Carrito)[0]
print(campo.default) # <dataclasses._MISSING_TYPE object> (sin valor fijo)
print(campo.default_factory) # <class 'list'>
print(campo.default_factory()) # [] — llamada directa a la factory
El __init__ generado se parece esquemáticamente a esto. El código real usa dataclasses._HAS_DEFAULT_FACTORY como sentinel (que se muestra como <factory>):
def __init__(self, articulos=_HAS_DEFAULT_FACTORY):
if articulos is _HAS_DEFAULT_FACTORY:
self.articulos = list() # llamada a la factory
else:
self.articulos = articulos
El sentinel distingue “el llamador no pasó ningún valor” de “el llamador pasó None”. Esto significa que se puede pasar una lista explícita al instanciar y la factory no será llamada:
articulos_existentes = ["pan", "leche"]
c = Carrito(articulos=articulos_existentes)
print(c.articulos) # ['pan', 'leche'] — la factory no fue llamada
default vs default_factory: la regla
Para un valor inmutable, la asignación directa es la sintaxis correcta:
@dataclass
class Producto:
count: int = 1
label: str = "desconocido"
field() solo entra en juego en dos situaciones: un valor mutable, o un valor inmutable combinado con opciones de configuración del campo.
@dataclass
class Producto:
count: int = 1 # asignación directa, sin field()
internal_id: int = field(default=1, repr=False) # excluido del __repr__
created_by: str = field(default="system", init=False) # no configurable en __init__
articulos: list = field(default_factory=list) # mutable, factory obligatoria
repr=False excluye el campo de la representación mostrada por print(). init=False impide pasar el valor en la instanciación, Python usa siempre el valor por defecto. Son opciones de comportamiento, no de valor, y requieren el wrapper field().
| Caso | Sintaxis | Ejemplo |
|---|---|---|
| Valor inmutable | Asignación directa | count: int = 1 |
Valor inmutable + comportamiento (repr, init, compare…) | field(default=) | field(default=1, repr=False) |
| Valor mutable (list, dict, set, …) | field(default_factory=) | field(default_factory=list) |
| Valor calculado dinámicamente | field(default_factory=) con lambda | field(default_factory=lambda: uuid.uuid4().hex) |
Casos de uso reales
Identificadores auto-generados:
import uuid
from dataclasses import dataclass, field
@dataclass
class Transaccion:
id: str = field(default_factory=lambda: uuid.uuid4().hex)
monto: float = 0.0
uuid.uuid4().hex devuelve un UUID sin guiones (32 caracteres hexadecimales). Cada instancia recibe un identificador único sin necesidad de pasarlo explícitamente.
Configuración con valores por defecto no triviales:
from dataclasses import dataclass, field
from datetime import datetime
@dataclass
class InformeAuditoria:
errores: list[str] = field(default_factory=list)
metadata: dict = field(default_factory=dict)
creado_en: datetime = field(default_factory=datetime.now)
datetime.now se pasa como referencia de función, sin paréntesis. Cada informe recibe la marca de tiempo de su creación, no un valor congelado en el momento de definición de la clase.
Factory personalizada para objetos inicializados:
from dataclasses import dataclass, field
def config_por_defecto() -> dict:
return {"debug": False, "timeout": 30, "retries": 3}
@dataclass
class ServicioHTTP:
url: str
config: dict = field(default_factory=config_por_defecto)
Modificar servicio.config["debug"] = True no afecta a otras instancias. Cada una recibe su propia copia del diccionario de configuración.
Una nota sobre Python 3.10+ y dataclass(slots=True)
Desde Python 3.10, @dataclass(slots=True) genera automáticamente __slots__. default_factory sigue funcionando con normalidad: el mecanismo de inicialización es idéntico, solo cambia el almacenamiento de los atributos.
from dataclasses import dataclass, field
@dataclass(slots=True)
class Carrito:
articulos: list = field(default_factory=list)
Esta es la combinación recomendada cuando se quieren a la vez valores por defecto dinámicos y la optimización de memoria de __slots__. Para benchmarks concretos de memoria y las trampas de herencia, ver Python slots: optimizar la memoria de las instancias.
Conclusión
default_factory resuelve un problema fundamental de Python: los objetos mutables como valores por defecto se comparten entre todas las instancias de una clase. El mecanismo interno es simple (una llamada a un callable en el momento de __init__), pero protege contra una categoría de bugs difíciles de diagnosticar, ya que el estado compartido inesperado solo se hace visible cuando se modifica el valor. Las factories personalizadas van más lejos: permiten inicializar atributos con cualquier lógica, convirtiendo @dataclass en una herramienta apta para casos mucho más allá de los simples contenedores de datos.
