Qué son las dataclasses y por qué existen
Una dataclass es una clase cuyo propósito principal es contener datos. Antes de Python 3.7, escribir una clase así implicaba un boilerplate considerable:
class Punto:
def __init__(self, x: int, y: int):
self.x = x
self.y = y
def __repr__(self) -> str:
return f"Punto(x={self.x}, y={self.y})"
def __eq__(self, otro) -> bool:
if not isinstance(otro, Punto):
return NotImplemented
return self.x == otro.x and self.y == otro.y
El módulo dataclasses elimina esa repetición. Con un decorador, Python genera __init__, __repr__ y __eq__ automáticamente a partir de las anotaciones de tipo:
from dataclasses import dataclass
@dataclass
class Punto:
x: int
y: int
Equivalente al código anterior, mucho más claro. Los métodos generados hacen exactamente lo esperado:
p = Punto(3, 5)
print(p) # Punto(x=3, y=5)
p == Punto(3, 5) # True
p == Punto(3, 6) # False
Las dataclasses son el equivalente Python a los records de Java, las case classes de Scala o los structs de Rust: la forma moderna de declarar un tipo de datos sin escribir código repetitivo.
Campos con valores por defecto
Las dataclasses admiten valores por defecto como cualquier firma de función:
@dataclass
class Usuario:
nombre: str
edad: int = 0
activo: bool = True
Los parámetros sin default deben ir antes de los que tienen default, igual que en funciones normales:
# Error: falla al declarar la clase
@dataclass
class Mal:
activo: bool = True
nombre: str # error: obligatorio tras uno con default
field: control fino de cada atributo
Para casos que necesitan más configuración que un simple default, se usa field:
from dataclasses import dataclass, field
@dataclass
class Pedido:
id: int
items: list[str] = field(default_factory=list)
metadatos: dict[str, str] = field(default_factory=dict)
default_factory es necesario para valores mutables por defecto (listas, diccionarios, sets). Si usaras items: list[str] = [], todas las instancias compartirían la misma lista, un bug clásico.
Otras opciones útiles de field:
init=False: el campo no aparece en__init__. Se inicializa en__post_init__o por asignación posterior.repr=False: el campo no aparece en__repr__(útil para datos sensibles o voluminosos).compare=False: el campo no participa en__eq__(ni en ordenación).metadata={...}: diccionario de metadatos para herramientas externas.
Ejemplo combinando opciones:
@dataclass
class Factura:
numero: str
total: float
tags: list[str] = field(default_factory=list)
_cache: dict = field(default_factory=dict, init=False, repr=False, compare=False)
post_init: lógica de inicialización
Si necesitas ejecutar código tras construir el objeto, define __post_init__:
@dataclass
class Rectangulo:
ancho: float
alto: float
area: float = field(init=False)
def __post_init__(self) -> None:
self.area = self.ancho * self.alto
__post_init__ corre después del __init__ generado. Aquí calculamos area como valor derivado, sin requerirlo en el constructor.
También es el lugar habitual para validación:
@dataclass
class Persona:
nombre: str
edad: int
def __post_init__(self) -> None:
if self.edad < 0:
raise ValueError("la edad no puede ser negativa")
if not self.nombre:
raise ValueError("el nombre no puede estar vacío")
frozen: inmutabilidad
Declarar frozen=True hace la dataclass inmutable: una vez creada, no se puede modificar. Asignar a un atributo lanza FrozenInstanceError.
@dataclass(frozen=True)
class Coordenada:
latitud: float
longitud: float
c = Coordenada(40.4, -3.7)
c.latitud = 41.0 # FrozenInstanceError
Además de la inmutabilidad, frozen=True habilita hashing por defecto, por lo que la instancia puede usarse como clave de diccionario o elemento de conjunto:
ciudades: dict[Coordenada, str] = {
Coordenada(40.4, -3.7): "Madrid",
Coordenada(41.4, 2.2): "Barcelona",
}
La inmutabilidad es recomendable por defecto para datos que representan valores del dominio, igual que strings o tuplas. El código se vuelve más predecible y seguro en concurrencia.
slots: ahorro de memoria
Con slots=True (disponible desde Python 3.10), la dataclass no usa el diccionario habitual de instancia sino un array fijo de atributos. Esto ahorra memoria y acelera el acceso.
@dataclass(slots=True)
class Punto:
x: int
y: int
La diferencia es notable en estructuras de datos grandes: un millón de instancias de Punto con slots=True ocupa del orden de la mitad de memoria que sin slots.
Limitación: no se pueden añadir atributos dinámicos en tiempo de ejecución (p.nuevo = 1 falla), lo que de hecho suele ser una ventaja porque captura typos.
order: dataclasses ordenables
Con order=True, Python genera también __lt__, __le__, __gt__, __ge__ basándose en una comparación por tuplas de los atributos:
@dataclass(order=True)
class Tarea:
prioridad: int
descripcion: str
tareas = [Tarea(3, "tarde"), Tarea(1, "urgente"), Tarea(2, "normal")]
tareas.sort()
# [Tarea(prioridad=1, ...), Tarea(prioridad=2, ...), Tarea(prioridad=3, ...)]
Si quieres controlar qué campos se usan para ordenar, combina con field(compare=False) para excluir algunos, o define el orden explícitamente con un sort key.
Comparación con namedtuple y TypedDict
Python tiene tres formas principales de representar datos:
| Herramienta | Inmutable | Mutable | Hereda métodos | Type hints |
|-------------|:---------:|:-------:|:--------------:|:----------:|
| namedtuple | Sí | No | Limitado | No nativo |
| TypedDict | Sí (dict) | Sí (dict) | No | Sí |
| dataclass | Opcional | Sí por defecto | Sí | Sí |
En 2026, dataclass es la opción por defecto en la mayoría de casos. TypedDict se usa cuando la estructura proviene de JSON o de una API y queremos tratarla como dict. namedtuple sobrevive por compatibilidad pero dataclass con frozen=True y slots=True la cubre mejor.
Conversión a dict o tupla
El módulo ofrece utilidades para serializar:
from dataclasses import asdict, astuple
@dataclass
class Usuario:
nombre: str
edad: int
u = Usuario("Ana", 30)
asdict(u) # {"nombre": "Ana", "edad": 30}
astuple(u) # ("Ana", 30)
asdict es recursivo: si un campo es una dataclass anidada, también se convierte. Muy útil para generar JSON o payloads de API.
Cuándo usar pydantic en lugar de dataclass
Las dataclasses son estructurales: definen la forma de los datos sin validar nada. Si trabajas en los bordes de la aplicación (entrada HTTP, lectura de JSON, formularios) y necesitas validación en tiempo de ejecución, la opción moderna es pydantic v2.
Como regla simple:
- Dataclass: datos ya validados dentro del dominio.
- Pydantic: datos que llegan desde fuera y hay que validar.
Ambas conviven bien: pydantic se usa en las APIs y los bordes; dataclasses en el núcleo de dominio donde los invariantes ya están asegurados.
Recapitulación
Las dataclasses reducen el boilerplate de las clases de datos y hacen el código más claro. Con frozen=True obtienes inmutabilidad, con slots=True ahorras memoria, con order=True las haces ordenables. Para la mayoría de clases cuyo propósito principal es contener datos, la opción correcta por defecto en Python moderno es una dataclass bien configurada.
Alan Sastre
Ingeniero de Software y formador, CEO en CertiDevs
Ingeniero de software especializado en Full Stack y en Inteligencia Artificial. Como CEO de CertiDevs, Python es una de sus áreas de expertise. Con más de 15 años programando, 6K seguidores en LinkedIn y experiencia como formador, Alan se dedica a crear contenido educativo de calidad para desarrolladores de todos los niveles.
Más tutoriales de Python
Explora más contenido relacionado con Python y continúa aprendiendo con nuestros tutoriales gratuitos.
Aprendizajes de esta lección
Declarar clases de datos con el decorador dataclass. Diferenciar campos obligatorios, con default y con factory. Usar field para configuraciones avanzadas y slots para ahorrar memoria. Hacer dataclasses inmutables con frozen y congeladas ordenables con order.