namedtuple y dataclasses en Python: datos estructurados sin clases completas

namedtuple y dataclass resuelven el mismo problema: estructurar datos relacionados en un objeto con campos nombrados, sin tener que escribir una clase completa a mano. Pero lo hacen de formas distintas, con diferentes compromisos en mutabilidad, herencia y funcionalidades extra.

namedtuple: tupla con campos nombrados

from collections import namedtuple

# Define el tipo y sus campos
Punto = namedtuple("Punto", ["x", "y"])
Color = namedtuple("Color", "rojo verde azul")  # también con string

p = Punto(3, 4)
print(p.x, p.y)   # 3 4
print(p[0], p[1]) # 3 4 — también por índice
print(p)          # Punto(x=3, y=4)

# Son inmutables: p.x = 10  lanzaría AttributeError
# Son tuplas: len(p), p == (3, 4), list(p)

# _replace crea una copia con campos modificados
p2 = p._replace(x=10)
print(p2)         # Punto(x=10, y=4)
print(p)          # Punto(x=3, y=4) — original sin cambios

namedtuple: valores por defecto (Python 3.6.1+)

from collections import namedtuple

# defaults cubre los últimos N campos desde la derecha
Config = namedtuple("Config", ["host", "puerto", "ssl", "timeout"],
                   defaults=["localhost", 5432, True, 30])

c1 = Config()
print(c1)   # Config(host='localhost', puerto=5432, ssl=True, timeout=30)

c2 = Config("produccion.ejemplo.com", ssl=False)
print(c2)   # Config(host='produccion.ejemplo.com', puerto=5432, ssl=False, timeout=30)

dataclass: clases de datos modernas

from dataclasses import dataclass, field

@dataclass
class Empleado:
    nombre: str
    email: str
    salario: float = 30000.0
    habilidades: list = field(default_factory=list)  # evita el bug de mutable compartido

e = Empleado("Ana", "[email protected]", salario=45000.0)
e.habilidades.append("Python")

print(e)
# Empleado(nombre='Ana', email='[email protected]', salario=45000.0, habilidades=['Python'])

# __eq__ generado automáticamente
e2 = Empleado("Ana", "[email protected]", salario=45000.0)
print(e == e2)  # False (habilidades difieren)

dataclass frozen: inmutabilidad como namedtuple

from dataclasses import dataclass

@dataclass(frozen=True)
class Punto:
    x: float
    y: float

    def distancia_origen(self):
        return (self.x**2 + self.y**2) ** 0.5

p = Punto(3, 4)
print(p.distancia_origen())  # 5.0
# p.x = 10  # FrozenInstanceError

# frozen=True habilita __hash__, así que pueden usarse en sets y dicts
puntos = {Punto(0, 0), Punto(1, 1), Punto(0, 0)}
print(puntos)  # {Punto(x=0, y=0), Punto(x=1, y=1)}

dataclass con __post_init__: validación

from dataclasses import dataclass

@dataclass
class Transaccion:
    concepto: str
    importe: float
    tipo: str = "debito"  # 'debito' o 'credito'

    def __post_init__(self):
        if self.importe <= 0:
            raise ValueError(f"El importe debe ser positivo: {self.importe}")
        if self.tipo not in ("debito", "credito"):
            raise ValueError(f"Tipo no válido: {self.tipo}")
        # Normalizar
        self.concepto = self.concepto.strip()

t = Transaccion("  Pago alquiler  ", 800.0)
print(t.concepto)  # 'Pago alquiler'

Cuándo usar cada uno

# namedtuple:
# + Inmutable por defecto (seguro para usar como clave de dict)
# + Ligero: es una tupla, ocupa menos memoria
# + Interop con código que espera tuplas
# - Sin métodos propios (salvo con herencia)
# - Validación limitada

# dataclass:
# + Mutable por defecto (o frozen=True para inmutabilidad)
# + Validación en __post_init__
# + field(default_factory=...) para mutables
# + Herencia limpia
# + Conversión a dict con asdict()
# - Algo más verboso
# - No es una tupla

Para datos simples que nunca cambian y que pueden viajar entre funciones, namedtuple es la opción más ligera. Para datos con lógica de validación, campos opcionales complejos o métodos propios, las dataclass son la herramienta moderna.

COMPARTE ESTE ARTÍCULO

COMPARTIR EN FACEBOOK
COMPARTIR EN TWITTER
COMPARTIR EN LINKEDIN
COMPARTIR EN WHATSAPP