CAP 05 · LEC 03·Type hints

Protocols y duck typing: estructuras tipadas sin herencia

Python siempre tuvo duck typing: si camina como pato y grazna como pato, es un pato. Protocol (Python 3.8+) le da tipado formal a esa idea sin forzar herencia.

● INTERMEDIO9 min lectura4 ejerciciospor Fernando Herrera · actualizado mayo de 2026
¿Encontraste un error o algo que mejorar?Editá esta lección en GitHub →

Duck typing en la práctica

Duck typing significa que Python no comprueba la clase de un objeto, sino que comprueba si tiene los métodos y atributos que necesita. Si un objeto tiene el método read(), puede pasarse a cualquier función que espere «algo legible» — independientemente de su clase.

# No importa la clase, importa el comportamiento class Archivo: def leer(self) -> str: return "contenido del archivo" class ConexionHTTP: def leer(self) -> str: return "respuesta HTTP" class SocketTCP: def leer(self) -> str: return "datos del socket" # Esta función acepta cualquier objeto con método leer() def procesar(fuente) -> str: return fuente.leer().upper() # Todas funcionan — duck typing en acción print(procesar(Archivo())) # CONTENIDO DEL ARCHIVO print(procesar(ConexionHTTP())) # RESPUESTA HTTP print(procesar(SocketTCP())) # DATOS DEL SOCKET
SalidaCONTENIDO DEL ARCHIVO RESPUESTA HTTP DAT OS DEL SOCKET

El problema: el parámetro fuente no tiene tipo, mypy no puede ayudarte. Si alguien pasa un objeto sin leer(), el error aparece en runtime. Protocol soluciona esto.

Protocol — contratos sin herencia

Protocol define una interfaz estructural: «cualquier objeto que tenga estos métodos cumple este contrato». No hace falta heredar de Protocol — basta con tener los métodos correctos.

from typing import Protocol # Definimos el contrato: cualquier objeto con método leer() class Legible(Protocol): def leer(self) -> str: ... # solo la firma, sin implementación # Ahora la función está tipada def procesar(fuente: Legible) -> str: return fuente.leer().upper() # Estas clases NO heredan de Legible, pero lo satisfacen class Archivo: def leer(self) -> str: return "contenido del archivo" class Buffer: def leer(self) -> str: return "datos del buffer" # mypy verifica que ambas cumplen el Protocol ✅ print(procesar(Archivo())) # CONTENIDO DEL ARCHIVO print(procesar(Buffer())) # DATOS DEL BUFFER
SalidaCONTENIDO DEL ARCHIVO DAT OS DEL BUFFER

Un Protocol puede definir múltiples métodos y atributos:

from typing import Protocol class Serializable(Protocol): def to_json(self) -> str: ... def to_dict(self) -> dict: ... id: int # también puede requerir atributos # Cualquier clase con estos métodos y atributo satisface Serializable class Usuario: def __init__(self, user_id: int, nombre: str) -> None: self.id = user_id self.nombre = nombre def to_json(self) -> str: import json return json.dumps(self.to_dict()) def to_dict(self) -> dict: return {"id": self.id, "nombre": self.nombre} usuario = Usuario(1, "Ana") print(usuario.to_json()) # {"id": 1, "nombre": "Ana"}
Salida{"id": 1, "nombre": "Ana"}

runtime_checkable

Por defecto, los Protocol solo se verifican estáticamente por mypy. Si necesitas usar isinstance() en runtime, añade el decorador @runtime_checkable:

from typing import Protocol, runtime_checkable @runtime_checkable class Printable(Protocol): def __str__(self) -> str: ... class Producto: def __init__(self, nombre: str, precio: float) -> None: self.nombre = nombre self.precio = precio def __str__(self) -> str: return f"{self.nombre}: ${self.precio:.2f}" p = Producto("Teclado", 75.0) # isinstance funciona porque el Protocol es runtime_checkable print(isinstance(p, Printable)) # True print(isinstance(42, Printable)) # True — los int tienen __str__ print(str(p)) # Teclado: $75.00
SalidaTrue True Teclado: $75.00
isinstance con Protocol es superficial

@runtime_checkable con isinstance solo verifica que los métodos existan, no que tengan la firma correcta. Un objeto con __str__ = 42 pasaría la comprobación. Para validación completa, confía en mypy.

Comparación con ABC

Las clases base abstractas (ABC) también definen contratos, pero requieren herencia explícita. Protocol usa tipado estructural — no importa la jerarquía, importa la forma.

ABC (herencia nominal)Protocol (tipado estructural)
Requiere herencia explícita class Perro(Animal): ...Sin herencia necesaria class Perro: ... # tiene ladrar()
No funciona con clases de tercerosCompatible con cualquier clase existente
isinstance() funciona siempreisinstance() requiere @runtime_checkable
Ideal para jerarquías de dominio propiasIdeal para interfaces genéricas y código de terceros
from abc import ABC, abstractmethod from typing import Protocol # ABC: herencia obligatoria class FiguraABC(ABC): @abstractmethod def area(self) -> float: ... class CirculoABC(FiguraABC): # ❌ debe heredar de FiguraABC def area(self) -> float: return 3.14 * 5 ** 2 # Protocol: no hace falta heredar class TienePrecio(Protocol): def calcular_precio(self) -> float: ... class Producto: # ✅ solo necesita tener el método def calcular_precio(self) -> float: return 99.0 def mostrar_precio(item: TienePrecio) -> None: print(f"Precio: ${item.calcular_precio():.2f}") mostrar_precio(Producto()) # Precio: $99.00
SalidaPrecio: $99.00

Practica