Python Dataclasses
Aprende Python dataclasses: el decorador @dataclass, valores predeterminados con field(), ordenación, inmutabilidad y herencia con ejemplos claros.
Una dataclass es una clase Python normal cuyo código repetitivo — __init__, __repr__ y __eq__ — es generado automáticamente por el decorador @dataclass. El resultado es menos código, menos errores tipográficos y clases que son inmediatamente legibles.
Este capítulo cubre:
- Por qué existen las dataclasses y cuándo usarlas
- El decorador
@dataclass - Valores predeterminados de campos y el helper
field() - Control de igualdad y ordenación
- Dataclasses inmutables con
frozen=True - Lógica de post-inicialización con
__post_init__ - Herencia con dataclasses
- Dataclasses vs.
NamedTuplevs. clases simples
Antes de leer este capítulo, asegúrate de estar familiarizado con las clases y objetos de Python y la herencia en Python.
¿Por qué Dataclasses?
Considera una clase que almacena un producto en una tienda en línea. Sin dataclasses, escribes las mismas asignaciones de atributos tres veces — una en __init__, una en __repr__ y una en __eq__:
class Product:
def __init__(self, name, price, stock):
self.name = name
self.price = price
self.stock = stock
def __repr__(self):
return f"Product(name={self.name!r}, price={self.price}, stock={self.stock})"
def __eq__(self, other):
if not isinstance(other, Product):
return NotImplemented
return (self.name, self.price, self.stock) == (other.name, other.price, other.stock)El decorador @dataclass genera todo lo anterior a partir de una única lista anotada de campos:
from dataclasses import dataclass
@dataclass
class Product:
name: str
price: float
stock: intAmbas versiones se comportan de forma idéntica. La versión con dataclass es más corta, más difícil de equivocarse y comunica de inmediato que esta clase es principalmente un contenedor de datos.
El Decorador @dataclass
Importa dataclass del módulo estándar dataclasses y aplícalo a tu clase. Cada campo se declara como una variable de clase con anotación de tipo:
from dataclasses import dataclass
@dataclass
class Point:
x: float
y: float
p = Point(1.5, 2.0)
print(p) # Point(x=1.5, y=2.0)
print(p.x) # 1.5
p2 = Point(1.5, 2.0)
print(p == p2) # True — __eq__ compares field by fieldEl decorador genera:
| Método | Qué hace |
|---|---|
__init__ | Acepta cada campo como parámetro y lo asigna a self |
__repr__ | Devuelve una cadena legible como Point(x=1.5, y=2.0) |
__eq__ | Compara dos instancias campo por campo |
Las anotaciones de tipo son obligatorias pero no se verifican en tiempo de ejecución
Las declaraciones de campo requieren una anotación de tipo (x: float). Python no comprueba el tipo en tiempo de ejecución — puedes seguir pasando una cadena donde se espera un float. La anotación es metadato utilizado por verificadores de tipos como mypy y por la propia maquinaria de dataclasses. Para validación de tipos en tiempo de ejecución, consulta Python Type Hints.
Valores Predeterminados
Asigna un valor predeterminado directamente al campo para hacerlo opcional en __init__:
from dataclasses import dataclass
@dataclass
class Config:
host: str = "localhost"
port: int = 8080
debug: bool = False
c1 = Config()
print(c1) # Config(host='localhost', port=8080, debug=False)
c2 = Config(host="example.com", port=443)
print(c2) # Config(host='example.com', port=443, debug=False)Los campos con valores predeterminados deben aparecer después de los campos sin valores predeterminados — exactamente la misma regla que para los parámetros de función normales.
Valores predeterminados mutables y field()
No puedes usar un objeto mutable (una lista, dict o set) como valor predeterminado simple. Python compartiría una sola lista entre todas las instancias, lo que lleva a errores sutiles:
from dataclasses import dataclass
# This raises a ValueError at class definition time:
# @dataclass
# class Bag:
# items: list = [] # ValueError: mutable default is not allowedEn su lugar, usa field(default_factory=...) para crear un objeto nuevo para cada instancia:
from dataclasses import dataclass, field
@dataclass
class Bag:
items: list = field(default_factory=list)
b1 = Bag()
b2 = Bag()
b1.items.append("apple")
print(b1.items) # ['apple']
print(b2.items) # [] — b2 has its own separate listdefault_factory acepta cualquier callable sin argumentos, incluidas lambdas y tus propias funciones.
El Helper field()
field() te ofrece control detallado sobre cada campo. Sus parámetros más útiles son:
| Parámetro | Propósito |
|---|---|
default | Un valor predeterminado simple (solo escalares) |
default_factory | Un callable que produce el valor predeterminado |
repr | False para excluir este campo de __repr__ |
compare | False para excluir este campo de __eq__ (y ordenación) |
init | False para excluir este campo de __init__ |
from dataclasses import dataclass, field
import time
@dataclass
class LogEntry:
message: str
level: str = "INFO"
timestamp: float = field(default_factory=time.time, repr=False, compare=False)
entry = LogEntry("Server started")
print(entry) # LogEntry(message='Server started', level='INFO')
# timestamp exists but is hidden from repr and ignored in comparisons
print(entry.timestamp > 0) # TrueOrdenación
Por defecto, las dataclasses admiten igualdad (==, !=) pero no ordenación (<, >, <=, >=). Activa la ordenación pasando order=True al decorador:
from dataclasses import dataclass
@dataclass(order=True)
class Version:
major: int
minor: int
patch: int
v1 = Version(1, 2, 0)
v2 = Version(1, 3, 0)
v3 = Version(1, 2, 0)
print(v1 < v2) # True
print(v1 == v3) # True
print(v2 > v1) # True
versions = [Version(2, 0, 0), Version(1, 9, 1), Version(1, 2, 3)]
print(sorted(versions))
# [Version(major=1, minor=2, patch=3),
# Version(major=1, minor=9, patch=1),
# Version(major=2, minor=0, patch=0)]Python genera los métodos de comparación comparando los campos en el orden en que se declaran, al estilo de tuplas. Puedes excluir un campo de las comparaciones con field(compare=False).
Dataclasses Inmutables con frozen=True
Pasa frozen=True para hacer que todos los campos sean de solo lectura tras la creación. Cualquier intento de modificar un campo lanza un FrozenInstanceError:
from dataclasses import dataclass
@dataclass(frozen=True)
class Coordinate:
lat: float
lon: float
london = Coordinate(51.5074, -0.1278)
print(london) # Coordinate(lat=51.5074, lon=-0.1278)
# london.lat = 0.0 # FrozenInstanceError: cannot assign to field 'lat'Las dataclasses congeladas también son hasheables (implementan __hash__), por lo que puedes usarlas como claves de diccionario o miembros de conjunto:
from dataclasses import dataclass
@dataclass(frozen=True)
class Coordinate:
lat: float
lon: float
cities = {
Coordinate(51.5074, -0.1278): "London",
Coordinate(48.8566, 2.3522): "Paris",
}
print(cities[Coordinate(51.5074, -0.1278)]) # LondonLas dataclasses normales (mutables) no son hasheables por defecto — Python establece __hash__ a None cuando se define __eq__ sin frozen=True.
Lógica de Post-Inicialización con __post_init__
A veces necesitas derivar el valor de un campo a partir de otros campos, o validar la entrada después de que se ejecute __init__. Define un método __post_init__ — se llama automáticamente al final del __init__ generado:
from dataclasses import dataclass, field
import math
@dataclass
class Circle:
radius: float
def __post_init__(self):
if self.radius <= 0:
raise ValueError(f"radius must be positive, got {self.radius}")
@property
def area(self):
return math.pi * self.radius ** 2
c = Circle(5)
print(round(c.area, 4)) # 78.5398
# Circle(-1) # ValueError: radius must be positive, got -1También puedes calcular un campo derivado. Márcalo con field(init=False) para que no aparezca en __init__, y luego establécelo dentro de __post_init__:
from dataclasses import dataclass, field
@dataclass
class Rectangle:
width: float
height: float
area: float = field(init=False, repr=True)
def __post_init__(self):
self.area = self.width * self.height
r = Rectangle(4, 6)
print(r) # Rectangle(width=4, height=6, area=24)
print(r.area) # 24Herencia con Dataclasses
Una dataclass puede heredar de otra dataclass. El __init__ de la clase hija incluye los campos de ambas clases — primero los campos del padre, en el orden en que fueron declarados:
from dataclasses import dataclass
@dataclass
class Animal:
name: str
age: int
@dataclass
class Dog(Animal):
breed: str
rex = Dog(name="Rex", age=3, breed="Labrador")
print(rex) # Dog(name='Rex', age=3, breed='Labrador')Precaución: si una clase padre tiene un campo con valor predeterminado, todos los campos de la clase hija también deben tener valores predeterminados. Esta es la misma regla que se aplica a las firmas de funciones Python normales — un parámetro sin valor predeterminado no puede seguir a uno que sí lo tenga.
from dataclasses import dataclass
@dataclass
class Animal:
name: str
age: int = 0 # has a default
# @dataclass
# class Dog(Animal):
# breed: str # TypeError: non-default argument 'breed' follows default argumentSoluciona esto dando también un valor predeterminado al campo de la clase hija, o reestructurando la jerarquía para que los campos con valores predeterminados vengan al final.
Parámetros del Decorador de un Vistazo
@dataclass(
init=True, # generate __init__ (default True)
repr=True, # generate __repr__ (default True)
eq=True, # generate __eq__ (default True)
order=False, # generate <, >, <=, >= (default False)
frozen=False, # make fields immutable (default False)
)
class MyClass:
...Rara vez necesitas modificar la mayoría de estos. Los más comunes son order=True y frozen=True.
Funciones de Utilidad
El módulo dataclasses también proporciona tres funciones muy útiles:
fields()
Devuelve una tupla de objetos Field que describen cada campo de la clase:
from dataclasses import dataclass, fields
@dataclass
class Point:
x: float
y: float
for f in fields(Point):
print(f.name, f.type)
# x <class 'float'>
# y <class 'float'>asdict()
Convierte una instancia de dataclass en un diccionario simple (de forma recursiva):
from dataclasses import dataclass, asdict
@dataclass
class Address:
street: str
city: str
@dataclass
class Person:
name: str
address: Address
p = Person("Alice", Address("10 Downing St", "London"))
print(asdict(p))
# {'name': 'Alice', 'address': {'street': '10 Downing St', 'city': 'London'}}Esto es útil para serializar a JSON o enviar datos a una API.
astuple()
Convierte a una tupla (de forma recursiva):
from dataclasses import dataclass, astuple
@dataclass
class Point:
x: float
y: float
p = Point(3.0, 4.0)
print(astuple(p)) # (3.0, 4.0)Dataclasses vs. NamedTuple vs. Clases Simples
| Característica | Clase simple | NamedTuple | dataclass |
|---|---|---|---|
__init__ automático | No | Sí | Sí |
__repr__ automático | No | Sí | Sí |
__eq__ automático | No | Sí (por valor) | Sí (por valor) |
| Mutable | Sí | No | Sí (por defecto) |
| Hasheable | No (si se define __eq__) | Sí | Solo con frozen=True |
| Ordenación | Manual | Sí | order=True |
| Herencia | Sí | Limitada | Sí |
Comprobación isinstance | Sí | Sí (también tuple) | Sí |
Desempaquetado (a, b = obj) | No | Sí | No |
Usa una dataclass cuando:
- Quieras datos mutables con inmutabilidad opcional.
- Necesites herencia o lógica post-init.
- Quieras control detallado de los campos (
field()).
Usa NamedTuple cuando:
- Quieras un registro inmutable que también se comporte como una tupla (desempaquetado posicional, filas CSV).
- Necesites compatibilidad con código que espera tuplas.
Usa una clase simple cuando:
- La clase tiene un comportamiento significativo y muy pocos datos simples.
- Necesitas un
__init__personalizado que no se pueda expresar mediante__post_init__.
Errores Comunes
Valores predeterminados mutables. Usar una lista o dict como valor predeterminado simple lanza ValueError en tiempo de definición de clase. Usa siempre field(default_factory=...).
Hasheo. Las dataclasses normales no son hasheables. Si las necesitas como claves de diccionario o en conjuntos, usa frozen=True o pasa unsafe_hash=True (raramente recomendado).
eq=False. Si deshabilitas la generación de igualdad (eq=False), Python recurre a la comparación por identidad (is), que casi nunca es lo que deseas para objetos de datos.
Orden de valores predeterminados en la herencia. Si un campo del padre tiene un valor predeterminado y un campo del hijo no lo tiene, Python lanza un TypeError. Planifica cuidadosamente el orden de los campos en tu jerarquía.
Resumen
| Concepto | Qué hace |
|---|---|
@dataclass | Genera __init__, __repr__, __eq__ automáticamente |
field() | Control detallado de campos: valores predeterminados, repr, compare, init |
default_factory | Proporciona un valor mutable nuevo para cada instancia |
order=True | Añade <, >, <=, >= basándose en el orden de los campos |
frozen=True | Hace los campos de solo lectura y la instancia hasheable |
__post_init__ | Se ejecuta después de __init__ para validación o campos derivados |
fields() | Devuelve metadatos sobre cada campo |
asdict() | Convierte la instancia en un dict simple (recursivamente) |
astuple() | Convierte la instancia en una tupla simple (recursivamente) |
Para temas relacionados, consulta las clases y objetos de Python, la herencia en Python y las clases base abstractas de Python.