W3docs

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. NamedTuple vs. 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: int

Ambas 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 field

El decorador genera:

MétodoQué 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 allowed

En 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 list

default_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ámetroPropósito
defaultUn valor predeterminado simple (solo escalares)
default_factoryUn callable que produce el valor predeterminado
reprFalse para excluir este campo de __repr__
compareFalse para excluir este campo de __eq__ (y ordenación)
initFalse 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) # True

Ordenació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)])   # London

Las 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 -1

Tambié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)    # 24

Herencia 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 argument

Soluciona 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ísticaClase simpleNamedTupledataclass
__init__ automáticoNo
__repr__ automáticoNo
__eq__ automáticoNoSí (por valor)Sí (por valor)
MutableNoSí (por defecto)
HasheableNo (si se define __eq__)Solo con frozen=True
OrdenaciónManualorder=True
HerenciaLimitada
Comprobación isinstanceSí (también tuple)
Desempaquetado (a, b = obj)NoNo

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

ConceptoQué hace
@dataclassGenera __init__, __repr__, __eq__ automáticamente
field()Control detallado de campos: valores predeterminados, repr, compare, init
default_factoryProporciona un valor mutable nuevo para cada instancia
order=TrueAñade <, >, <=, >= basándose en el orden de los campos
frozen=TrueHace 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.

Práctica

Práctica
Which decorator do you use to create a dataclass in Python?
Which decorator do you use to create a dataclass in Python?
Was this page helpful?