Lección 14 · 14 de 20
Anotaciones de tipo, protocolos y genéricos · Python
Anotaciones de tipo en Python: alias con type, typing.Protocol para el tipado estructural y funciones genéricas con la sintaxis de Python 3.12.

Las anotaciones de tipo (title: str, -> Euros) documentan qué espera y qué devuelve cada función. Python no las comprueba al ejecutar; las usan los editores y herramientas como mypy o Pyright para avisar de errores antes de ejecutar el programa. type Euros = float crea un alias: un nombre más expresivo para un tipo que ya existe.
Un `Protocol` describe una forma, no una familia: Priced dice que vale cualquier objeto con un método price() que devuelva euros. Book y Subscription no heredan de Priced y aun así lo cumplen. Es el tipado estructural (el *duck typing* de siempre, pero comprobable): si tiene price(), sirve.
Por eso basket_total(items: list[Priced], ...) acepta libros, suscripciones y cualquier clase futura que tenga price(). Con `@runtime_checkable`, además, isinstance(basket[1], Priced) se puede preguntar al ejecutar: la suscripción lo cumple y la cadena "Dune" no.
def first[T](items: list[T], default: T) -> T es una función genérica (sintaxis de Python 3.12; antes se usaba TypeVar). T es un tipo cualquiera, pero el mismo en toda la firma: con una lista de cadenas, el verificador sabe que el resultado es una cadena.
La última línea lo deja claro: Subscription("3") pasa una cadena donde se esperaba un int y Python lo ejecuta sin quejarse. Un verificador de tipos lo marcaría como error.
Código · main.py
from typing import Protocol, runtime_checkabletype Euros = float # a type alias (Python 3.12)@runtime_checkableclass Priced(Protocol): # any class with price() fits def price(self) -> Euros: ...class Book: # no inheritance from Priced needed def __init__(self, title: str, cost: Euros) -> None: self.title = title self.cost = cost def price(self) -> Euros: return self.costclass Subscription: def __init__(self, months: int) -> None: self.months = months def price(self) -> Euros: return self.months * 7.99def basket_total(items: list[Priced], vat: float = 0.04) -> Euros: return round(sum(item.price() for item in items) * (1 + vat), 2)def first[T](items: list[T], default: T) -> T: # a generic function return items[0] if items else defaultbasket: list[Priced] = [Book("Dune", 11.5), Subscription(3)]print("Total:", basket_total(basket))print("Is Priced:", isinstance(basket[1], Priced), isinstance("Dune", Priced))print("First:", first(["INV-1", "INV-2"], "none"), "|", first([], 0))print("Not enforced:", repr(Subscription("3").months)) # runs anywaySalida · python3 main.py
Total: 36.89 Is Priced: True False First: INV-1 | 0 Not enforced: '3'
Para recordar
- Las anotaciones no cambian lo que hace el programa: lo documentan.
type Nombre = ...crea un alias de tipo.- Un
Protocolacepta cualquier clase con los métodos indicados. def f[T](x: T) -> Tdeclara una función genérica.- Use mypy o Pyright para que las anotaciones detecten errores.
Ejercicio
Cree una clase GiftCard con price() y añádala a basket sin heredar de nada; después escriba def last[T](items: list[T], default: T) -> T.