Skip to content

Dataclasses en Python: Una guía completa del decorador @dataclass

Updated on

Escribir clases en Python a menudo implica código repetitivo (boilerplate). Defines __init__ para inicializar atributos, __repr__ para una salida legible, __eq__ para comparaciones y, a veces, __hash__ para poder usar hashing. Esta implementación manual se vuelve tediosa para clases que solo contienen datos, especialmente al gestionar objetos de configuración, respuestas de APIs o registros de base de datos.

Python 3.7 introdujo las dataclasses mediante la PEP 557, automatizando este boilerplate sin perder la flexibilidad de las clases normales. El decorador @dataclass genera métodos especiales automáticamente basándose en anotaciones de tipo, reduciendo el código de docenas de líneas a solo unas pocas. Esta guía muestra cómo aprovechar dataclasses para obtener código Python más limpio y mantenible.

📚

Por qué existen las dataclasses: resolver el problema del boilerplate

Las clases tradicionales de Python requieren definir explícitamente métodos para operaciones comunes. Considera esta clase estándar para almacenar datos de usuario:

class User:
    def __init__(self, name, email, age):
        self.name = name
        self.email = email
        self.age = age
 
    def __repr__(self):
        return f"User(name={self.name!r}, email={self.email!r}, age={self.age!r})"
 
    def __eq__(self, other):
        if not isinstance(other, User):
            return NotImplemented
        return (self.name, self.email, self.age) == (other.name, other.email, other.age)

Con dataclasses, esto se reduce a:

from dataclasses import dataclass
 
@dataclass
class User:
    name: str
    email: str
    age: int

El decorador genera __init__, __repr__ y __eq__ automáticamente a partir de las anotaciones de tipo. Esto elimina más de 15 líneas de boilerplate manteniendo la misma funcionalidad.

Sintaxis básica de @dataclass

La dataclass más simple solo requiere anotaciones de tipo para los campos:

from dataclasses import dataclass
 
@dataclass
class Product:
    name: str
    price: float
    quantity: int
 
product = Product("Laptop", 999.99, 5)
print(product)  # Product(name='Laptop', price=999.99, quantity=5)
 
product2 = Product("Laptop", 999.99, 5)
print(product == product2)  # True

El decorador acepta parámetros para personalizar el comportamiento:

@dataclass(
    init=True,       # Genera __init__ (por defecto: True)
    repr=True,       # Genera __repr__ (por defecto: True)
    eq=True,         # Genera __eq__ (por defecto: True)
    order=False,     # Genera métodos de comparación (por defecto: False)
    frozen=False,    # Hace las instancias inmutables (por defecto: False)
    unsafe_hash=False  # Genera __hash__ (por defecto: False)
)
class Config:
    host: str
    port: int

Tipos de campo y valores por defecto

Las dataclasses admiten valores por defecto en campos. Los campos sin valores por defecto deben aparecer antes que los campos con valores por defecto:

from dataclasses import dataclass
 
@dataclass
class Server:
    host: str
    port: int = 8080
    protocol: str = "http"
 
server1 = Server("localhost")
print(server1)  # Server(host='localhost', port=8080, protocol='http')
 
server2 = Server("api.example.com", 443, "https")
print(server2)  # Server(host='api.example.com', port=443, protocol='https')

Para valores por defecto mutables como listas o diccionarios, usa default_factory para evitar referencias compartidas:

from dataclasses import dataclass, field
 
# WRONG - all instances share the same list
@dataclass
class WrongConfig:
    tags: list = []  # Raises error in Python 3.10+
 
# CORRECT - each instance gets a new list
@dataclass
class CorrectConfig:
    tags: list = field(default_factory=list)
    metadata: dict = field(default_factory=dict)
 
config1 = CorrectConfig()
config2 = CorrectConfig()
 
config1.tags.append("production")
print(config1.tags)  # ['production']
print(config2.tags)  # [] - separate list

La función field(): configuración avanzada de campos

La función field() ofrece control granular sobre campos individuales:

from dataclasses import dataclass, field
from typing import List
 
@dataclass
class Employee:
    name: str
    employee_id: int
    salary: float = field(repr=False)  # Oculta salary en repr
    skills: List[str] = field(default_factory=list)
    _internal_id: str = field(init=False, repr=False)  # No está en __init__
    performance_score: float = field(default=0.0, compare=False)  # Se excluye de la comparación
 
    def __post_init__(self):
        self._internal_id = f"EMP_{self.employee_id:06d}"
 
emp = Employee("Alice", 12345, 85000.0, ["Python", "SQL"])
print(emp)  # Employee(name='Alice', employee_id=12345, skills=['Python', 'SQL'], performance_score=0.0)
print(emp._internal_id)  # EMP_012345

Parámetros clave de field():

ParameterTypeDescription
defaultAnyValor por defecto para el campo
default_factoryCallableFunción sin argumentos que devuelve el valor por defecto
initboolIncluye el campo en __init__ (por defecto: True)
reprboolIncluye el campo en __repr__ (por defecto: True)
compareboolIncluye el campo en métodos de comparación (por defecto: True)
hashboolIncluye el campo en __hash__ (por defecto: None)
metadatadictMetadatos arbitrarios (no los usa el módulo dataclasses)
kw_onlyboolHace el campo solo-de-palabra-clave (Python 3.10+)

El parámetro metadata almacena información arbitraria accesible mediante fields():

from dataclasses import dataclass, field, fields
 
@dataclass
class APIRequest:
    endpoint: str = field(metadata={"description": "API endpoint path"})
    method: str = field(default="GET", metadata={"choices": ["GET", "POST", "PUT", "DELETE"]})
 
for f in fields(APIRequest):
    print(f"{f.name}: {f.metadata}")
# endpoint: {'description': 'API endpoint path'}
# method: {'choices': ['GET', 'POST', 'PUT', 'DELETE']}

Anotaciones de tipo con dataclasses

Las dataclasses dependen de anotaciones de tipo, pero no las aplican en tiempo de ejecución. Usa el módulo typing para tipos complejos:

from dataclasses import dataclass
from typing import List, Dict, Optional, Union, Tuple
from datetime import datetime
 
@dataclass
class DataAnalysisJob:
    job_id: str
    dataset_path: str
    columns: List[str]
    filters: Dict[str, Union[str, int, float]]
    output_format: str = "csv"
    created_at: datetime = field(default_factory=datetime.now)
    completed_at: Optional[datetime] = None
    error_message: Optional[str] = None
    results: Optional[Dict[str, Tuple[float, float]]] = None
 
job = DataAnalysisJob(
    job_id="job_001",
    dataset_path="/data/sales.csv",
    columns=["date", "revenue", "region"],
    filters={"year": 2026, "region": "US"}
)

Para verificación de tipos en runtime, intégralo con librerías como pydantic o usa validación en __post_init__.

frozen=True: crear dataclasses inmutables

Establece frozen=True para hacer las instancias inmutables tras su creación, similar a los named tuples:

from dataclasses import dataclass
 
@dataclass(frozen=True)
class Point:
    x: float
    y: float
 
    def distance_from_origin(self):
        return (self.x**2 + self.y**2) ** 0.5
 
point = Point(3.0, 4.0)
print(point.distance_from_origin())  # 5.0
 
# Attempting to modify raises FrozenInstanceError
try:
    point.x = 5.0
except AttributeError as e:
    print(f"Error: {e}")  # Error: cannot assign to field 'x'

Las dataclasses congeladas (frozen) son hashable por defecto si todos los campos son hashable, lo que permite usarlas en sets y como claves de diccionario:

@dataclass(frozen=True)
class Coordinate:
    latitude: float
    longitude: float
 
locations = {
    Coordinate(40.7128, -74.0060): "New York",
    Coordinate(51.5074, -0.1278): "London"
}
 
print(locations[Coordinate(40.7128, -74.0060)])  # New York

Método post_init: validación y campos calculados

El método __post_init__ se ejecuta después de __init__, permitiendo validación e inicialización de campos calculados:

from dataclasses import dataclass, field
from datetime import datetime
 
@dataclass
class BankAccount:
    account_number: str
    balance: float
    created_at: datetime = field(default_factory=datetime.now)
    account_type: str = field(init=False)
 
    def __post_init__(self):
        if self.balance < 0:
            raise ValueError("Initial balance cannot be negative")
 
        # Compute account_type based on balance
        if self.balance >= 100000:
            self.account_type = "Premium"
        elif self.balance >= 10000:
            self.account_type = "Gold"
        else:
            self.account_type = "Standard"
 
account = BankAccount("ACC123456", 50000.0)
print(account.account_type)  # Gold

Para campos con init=False que dependen de otros campos, usa __post_init__:

from dataclasses import dataclass, field
 
@dataclass
class Rectangle:
    width: float
    height: float
    area: float = field(init=False)
    perimeter: float = field(init=False)
 
    def __post_init__(self):
        self.area = self.width * self.height
        self.perimeter = 2 * (self.width + self.height)
 
rect = Rectangle(5.0, 3.0)
print(f"Area: {rect.area}, Perimeter: {rect.perimeter}")  # Area: 15.0, Perimeter: 16.0

Herencia con dataclasses

Las dataclasses soportan herencia con fusión automática de campos:

from dataclasses import dataclass
 
@dataclass
class Animal:
    name: str
    age: int
 
@dataclass
class Dog(Animal):
    breed: str
    is_good_boy: bool = True
 
dog = Dog("Buddy", 5, "Golden Retriever")
print(dog)  # Dog(name='Buddy', age=5, breed='Golden Retriever', is_good_boy=True)

Las subclases heredan los campos del padre y pueden añadir nuevos. Los campos sin valores por defecto no pueden ir después de campos con valores por defecto a través de la herencia:

from dataclasses import dataclass
 
@dataclass
class BaseConfig:
    environment: str = "production"
 
# ERROR: Non-default field 'api_key' cannot follow default field 'environment'
# @dataclass
# class APIConfig(BaseConfig):
#     api_key: str
 
# CORRECT: Use default or rearrange fields
@dataclass
class APIConfig(BaseConfig):
    api_key: str = ""  # Provide default
    timeout: int = 30

Python 3.10+ introdujo kw_only para resolver esto:

from dataclasses import dataclass
 
@dataclass
class BaseConfig:
    environment: str = "production"
 
@dataclass(kw_only=True)
class APIConfig(BaseConfig):
    api_key: str  # Must be passed as keyword argument
    timeout: int = 30
 
config = APIConfig(api_key="secret_key_123")  # OK
# config = APIConfig("secret_key_123")  # TypeError

slots=True: eficiencia de memoria (Python 3.10+)

Python 3.10 añadió slots=True para definir __slots__, reduciendo la sobrecarga de memoria:

from dataclasses import dataclass
import sys
 
@dataclass
class RegularUser:
    username: str
    email: str
    age: int
 
@dataclass(slots=True)
class SlottedUser:
    username: str
    email: str
    age: int
 
regular = RegularUser("john", "john@example.com", 30)
slotted = SlottedUser("jane", "jane@example.com", 28)
 
print(f"Regular: {sys.getsizeof(regular.__dict__)} bytes")  # ~104 bytes
print(f"Slotted: {sys.getsizeof(slotted)} bytes")          # ~64 bytes

Las dataclasses con slots ofrecen un ahorro de memoria del 30–40% y acceso a atributos más rápido, pero sacrifican la posibilidad de añadir atributos dinámicamente:

regular.new_attribute = "allowed"  # OK
# slotted.new_attribute = "error"  # AttributeError

kw_only=True: campos solo de palabra clave (Python 3.10+)

Fuerza que todos los campos sean solo-de-palabra-clave para una instanciación más clara:

from dataclasses import dataclass
 
@dataclass(kw_only=True)
class DatabaseConnection:
    host: str
    port: int
    username: str
    password: str
    database: str = "default"
 
# Must use keyword arguments
conn = DatabaseConnection(
    host="localhost",
    port=5432,
    username="admin",
    password="secret"
)
 
# Positional arguments raise TypeError
# conn = DatabaseConnection("localhost", 5432, "admin", "secret")

Combina kw_only con control por campo:

from dataclasses import dataclass, field
 
@dataclass
class MixedArgs:
    required_positional: str
    optional_positional: int = 0
    required_keyword: str = field(kw_only=True)
    optional_keyword: bool = field(default=False, kw_only=True)
 
obj = MixedArgs("value", 10, required_keyword="kw_value")

Comparación: dataclass vs alternativas

FeaturedataclassnamedtupleTypedDictPydanticattrs
MutabilityMutable (default)ImmutableN/A (dict subclass)MutableConfigurable
Type validationAnnotations onlyNoAnnotations onlyRuntime validationRuntime validation
Default valuesYesYesNoYesYes
MethodsFull class supportLimitedNoFull class supportFull class support
InheritanceYesNoLimitedYesYes
Memory overheadModerateLowLowHigherModerate
Slots supportYes (3.10+)NoNoYesYes
PerformanceFastFastestFastSlower (validation)Fast
Built-inYes (3.7+)YesYes (3.8+)NoNo

Elige dataclasses para:

  • Proyectos estándar de Python sin dependencias
  • Contenedores de datos simples con type hints
  • Cuando se necesita flexibilidad entre frozen/mutable
  • Jerarquías con herencia

Elige Pydantic para:

  • Validación de request/response de APIs
  • Gestión de configuración con validación estricta
  • Generación de JSON schema

Elige namedtuple para:

  • Contenedores inmutables ligeros
  • Máxima eficiencia de memoria
  • Compatibilidad con Python < 3.7

Convertir a/desde diccionarios

Las dataclasses proporcionan asdict() y astuple() para serialización:

from dataclasses import dataclass, asdict, astuple
 
@dataclass
class Config:
    host: str
    port: int
    ssl_enabled: bool = True
 
config = Config("api.example.com", 443)
 
# Convert to dictionary
config_dict = asdict(config)
print(config_dict)  # {'host': 'api.example.com', 'port': 443, 'ssl_enabled': True}
 
# Convert to tuple
config_tuple = astuple(config)
print(config_tuple)  # ('api.example.com', 443, True)

Para dataclasses anidadas:

from dataclasses import dataclass, asdict
 
@dataclass
class Address:
    street: str
    city: str
    zipcode: str
 
@dataclass
class Person:
    name: str
    address: Address
 
person = Person("Alice", Address("123 Main St", "Springfield", "12345"))
person_dict = asdict(person)
print(person_dict)
# {'name': 'Alice', 'address': {'street': '123 Main St', 'city': 'Springfield', 'zipcode': '12345'}}

Dataclasses con serialización JSON

Las dataclasses no soportan serialización JSON de forma nativa, pero la integración es sencilla:

import json
from dataclasses import dataclass, asdict
from datetime import datetime
 
@dataclass
class Event:
    name: str
    timestamp: datetime
    attendees: int
 
    def to_json(self):
        data = asdict(self)
        # Custom serialization for datetime
        data['timestamp'] = self.timestamp.isoformat()
        return json.dumps(data)
 
    @classmethod
    def from_json(cls, json_str):
        data = json.loads(json_str)
        data['timestamp'] = datetime.fromisoformat(data['timestamp'])
        return cls(**data)
 
event = Event("Python Conference", datetime.now(), 500)
json_str = event.to_json()
print(json_str)
 
restored = Event.from_json(json_str)
print(restored)

Para escenarios complejos, usa la librería dataclasses-json o Pydantic.

Patrones del mundo real

Objetos de configuración

from dataclasses import dataclass, field
from typing import List
 
@dataclass
class AppConfig:
    app_name: str
    version: str
    debug: bool = False
    allowed_hosts: List[str] = field(default_factory=lambda: ["localhost"])
    database_url: str = "sqlite:///app.db"
    cache_timeout: int = 300
 
    def __post_init__(self):
        if self.debug:
            print(f"Running {self.app_name} v{self.version} in DEBUG mode")
 
config = AppConfig("DataAnalyzer", "2.1.0", debug=True)

Modelos de respuesta de API

from dataclasses import dataclass
from typing import List, Optional
from datetime import datetime
 
@dataclass
class APIResponse:
    status: str
    data: Optional[List[dict]] = None
    error_message: Optional[str] = None
    timestamp: datetime = field(default_factory=datetime.now)
 
    @property
    def is_success(self):
        return self.status == "success"
 
response = APIResponse("success", data=[{"id": 1, "name": "Dataset A"}])
print(response.is_success)  # True

Registros de base de datos con integración de PyGWalker

from dataclasses import dataclass, asdict
from typing import List
import pandas as pd
 
@dataclass
class SalesRecord:
    date: str
    product: str
    revenue: float
    region: str
    quantity: int
 
# Create sample data
records = [
    SalesRecord("2026-01-01", "Laptop", 1299.99, "US", 5),
    SalesRecord("2026-01-02", "Mouse", 29.99, "EU", 50),
    SalesRecord("2026-01-03", "Keyboard", 89.99, "US", 20),
]
 
# Convert to DataFrame for visualization with PyGWalker
df = pd.DataFrame([asdict(r) for r in records])
 
# Use PyGWalker for interactive data exploration
# import pygwalker as pyg
# walker = pyg.walk(df)
# This creates a Tableau-like interface to visualize your dataclass-based data

Las dataclasses destacan al estructurar datos antes de la visualización. PyGWalker convierte DataFrames en interfaces visuales interactivas, haciendo que los flujos de trabajo de análisis de datos basados en dataclasses sean más fluidos.

Benchmarks de rendimiento vs clases normales

import timeit
from dataclasses import dataclass
 
# Regular class
class RegularClass:
    def __init__(self, x, y, z):
        self.x = x
        self.y = y
        self.z = z
 
    def __repr__(self):
        return f"RegularClass(x={self.x}, y={self.y}, z={self.z})"
 
    def __eq__(self, other):
        return (self.x, self.y, self.z) == (other.x, other.y, other.z)
 
@dataclass
class DataClass:
    x: int
    y: int
    z: int
 
# Benchmark instantiation
regular_time = timeit.timeit(lambda: RegularClass(1, 2, 3), number=1000000)
dataclass_time = timeit.timeit(lambda: DataClass(1, 2, 3), number=1000000)
 
print(f"Regular class: {regular_time:.4f}s")
print(f"Dataclass: {dataclass_time:.4f}s")
# Dataclasses are typically 5-10% slower due to decorator overhead
# but provide significantly cleaner code

Con slots=True (Python 3.10+), las dataclasses igualan o superan el rendimiento de clases normales, reduciendo el uso de memoria en un 30–40%.

Patrones avanzados: ordenación personalizada de campos

from dataclasses import dataclass, field
 
def sort_by_priority(items):
    return sorted(items, key=lambda x: x.priority, reverse=True)
 
@dataclass(order=True)
class Task:
    priority: int
    name: str = field(compare=False)
    description: str = field(compare=False)
 
tasks = [
    Task(3, "Review PR", "Code review for feature X"),
    Task(1, "Write docs", "Documentation update"),
    Task(5, "Fix bug", "Critical production issue"),
]
 
sorted_tasks = sorted(tasks)
for task in sorted_tasks:
    print(f"Priority {task.priority}: {task.name}")
# Priority 1: Write docs
# Priority 3: Review PR
# Priority 5: Fix bug

Buenas prácticas y trampas comunes (gotchas)

  1. Usa siempre default_factory para valores mutables por defecto: nunca asignes [] o {} directamente
  2. Los type hints son obligatorios: las dataclasses dependen de anotaciones, no de valores
  3. El orden de campos importa: campos sin default antes que campos con default
  4. frozen=True para datos inmutables: útil para objetos hashable y seguridad en concurrencia
  5. Usa __post_init__ con moderación: demasiada lógica reduce la simplicidad de dataclass
  6. Considera slots=True para datasets grandes: gran ahorro de memoria en Python 3.10+
  7. Valida en __post_init__: las dataclasses no hacen cumplir tipos en runtime

Preguntas frecuentes (FAQ)

Conclusión

Las dataclasses de Python eliminan el boilerplate mientras preservan todo el poder de las clases. El decorador @dataclass genera automáticamente métodos de inicialización, representación y comparación, reduciendo el tiempo de desarrollo y la carga de mantenimiento. Desde objetos de configuración hasta modelos de API y registros de base de datos, las dataclasses ofrecen un enfoque limpio, con anotaciones de tipo, para clases que contienen datos.

Las ventajas clave incluyen generación automática de métodos, comportamiento de campos personalizable mediante field(), inmutabilidad con frozen=True, validación vía __post_init__ y eficiencia de memoria con slots=True. Aunque alternativas como namedtuples y Pydantic cubren casos de uso específicos, las dataclasses logran un equilibrio óptimo entre simplicidad y funcionalidad para la mayoría de proyectos Python.

Para flujos de trabajo de análisis de datos, combinar dataclasses con herramientas como PyGWalker crea pipelines potentes donde modelos de datos estructurados alimentan directamente visualizaciones interactivas, agilizando todo: desde la ingesta de datos hasta la generación de insights.

📚