Kodokon kodokon.com

Type hints y dataclasses: tipar el Python moderno en serio

Comprende lo que las anotaciones hacen realmente en tiempo de ejecución y saca el máximo partido a las dataclasses con frozen, slots y default_factory.

10 min · 3 preguntas

Abrir esta lección en Kodokon

Las anotaciones de tipo son metadatos, no restricciones: el intérprete las almacena en __annotations__ y nunca las comprueba en tiempo de ejecución. La comprobación es tarea de un analizador estático como mypy o pyright, antes de la ejecución. La consecuencia directa: un programa mal tipado se ejecuta sin protestar - y eso es intencionado. El tipado gradual te permite anotar poco a poco una base de código existente sin romper nada.

PYTHON
from typing import get_type_hints

def add(a: int, b: int) -> int:
    return a + b

print(add("py", "thon"))
print(add.__annotations__)
print(get_type_hints(add))
Las anotaciones no bloquean nada: la llamada "incorrecta" funciona.

Desde Python 3.9, los tipos integrados son directamente genéricos: escribe list[int] o dict[str, float] sin importar typing.List. Las uniones se escriben int | None desde la versión 3.10. Para ligar el tipo de entrada al tipo de salida, usa un TypeVar - Python 3.12 incluso ofrece la sintaxis compacta def first[T](items: list[T]) -> T. Un punto que a menudo se pasa por alto: from __future__ import annotations vuelve perezosa cada anotación (se almacena como cadena de texto), lo que permite referenciar clases definidas más abajo y reduce el coste en el momento de la importación.

PYTHON
from collections.abc import Iterable, Iterator
from typing import TypeVar

T = TypeVar("T")

def dedupe(items: Iterable[T]) -> Iterator[T]:
    seen: set[T] = set()
    for item in items:
        if item not in seen:
            seen.add(item)
            yield item

print(list(dedupe([3, 1, 3, 2, 1])))
El TypeVar garantiza que dedupe(list[int]) produce realmente ints.
PYTHON
from dataclasses import dataclass, field

@dataclass(frozen=True, slots=True)
class Point:
    x: float
    y: float

@dataclass
class Basket:
    items: list[str] = field(default_factory=list)
    total: int = 0

    def __post_init__(self) -> None:
        self.total = len(self.items)

point = Point(1.0, 2.0)
print(point, hash(point))
print(Basket(["book", "pen"]))
Point es inmutable, hashable y no tiene un __dict__ por instancia.

Tres parámetros convierten @dataclass en una herramienta lista para producción. frozen=True hace que las instancias sean inmutables y, combinado con la igualdad generada, hashables - utilizables como claves de diccionario. slots=True (Python 3.10+) elimina el __dict__ por instancia: menos memoria, acceso más rápido a los atributos, y cualquier asignación de atributo inesperada falla. Por último, kw_only=True obliga a llamar con argumentos por nombre de forma explícita. __post_init__ es el lugar donde van los invariantes y los campos derivados. Para un agregado inmutable sencillo, NamedTuple sigue teniendo sentido; en cuanto hay comportamiento o validación, prefiere la dataclass.

Prueba de conocimientos

Comprueba que has retenido los puntos clave de esta lección.

  1. ¿Qué ocurre en tiempo de ejecución cuando llamas a add('py', 'thon') mientras la función está anotada como (a: int, b: int) -> int?
    • Python lanza inmediatamente un TypeError
    • La llamada se ejecuta normalmente: las anotaciones nunca se comprueban en tiempo de ejecución
    • Python emite un RuntimeWarning y luego ejecuta la llamada
    • El módulo se niega a importarse
  2. ¿Por qué está prohibido items: list[str] = [] como valor por defecto de un campo de dataclass?
    • Porque list[str] no es una anotación válida para un campo
    • Porque las dataclasses solo toleran campos sin valor por defecto
    • Porque este valor por defecto mutable se compartiría entre todas las instancias: la dataclass lanza ValueError y exige field(default_factory=list)
    • Porque los valores por defecto deben definirse en __post_init__
  3. ¿Qué le hace frozen=True a una dataclass?
    • Las instancias se vuelven inmutables y, con la igualdad generada, hashables - utilizables como claves de diccionario
    • La clase ya no se puede heredar
    • Los campos se convierten en atributos de clase compartidos
    • Las anotaciones se comprueban en cada asignación