Protocol en vez de herencia: costuras de prueba estructurales en Python
¿Duck typing con verificador de tipos? El Protocol de Python hace ambas cosas. Desde Python 3.8, typing.Protocol ofrece subtipado estructural: cualquier objeto con la forma correcta satisface el tipo, y el checker lo verifica — sin clase base compartida, sin abc.ABC, sin importar tu clase dentro del doble de prueba.
El problema de las costuras por herencia
Para reemplazar una dependencia real por un fake en las pruebas, la jugada clásica es una clase base abstracta compartida. Pero eso acopla tres cosas que no deberían estar acopladas:
- La implementación real debe heredar de la base.
- El fake de prueba debe importar y heredar la misma base.
- Toda implementación nueva — incluidas las de código ajeno — tiene que adherirse explícitamente.
Ese acoplamiento se propaga. Las clases de terceros no pueden heredar tu ABC de forma retroactiva, así que terminas escribiendo adaptadores cuyo único trabajo es decir "sí, soy uno de esos".
La solución estructural
Define un Protocol con solo el método que necesitas. Cualquier objeto que lo implemente satisface el tipo — la clase nunca menciona el protocolo:
from typing import Protocol
class Renderer(Protocol):
def render(self, template: str) -> str: ...
class HtmlRenderer: # no base class
def render(self, template: str) -> str:
return f"<html>{template}</html>"
class FakeRenderer: # test double: just implement the method
def render(self, template: str) -> str:
return "stub"
def publish(r: Renderer, tpl: str) -> str:
return r.render(tpl)
publish(HtmlRenderer(), "x") # type-checks
publish(FakeRenderer(), "x") # type-checks too
La especificación de tipado es explícita: un tipo concreto es asignable a un protocolo si y solo si implementa todos los miembros del protocolo — heredar explícitamente está permitido, pero "no es necesario … a efectos de verificación de tipos". Tu fake sigue siendo una clase simple en el archivo de pruebas, y mypy/pyright igual detectan un método renombrado o un tipo de retorno incorrecto.
Reglas prácticas
| Situación | Usa |
|---|---|
| Costura de prueba para uno o dos métodos | Protocol — gana la interfaz mínima |
| Controlas todas las implementaciones y quieres comportamiento compartido | ABC con métodos concretos de ayuda |
| Clases de terceros deben satisfacer la interfaz | Protocol (no pueden heredar tu ABC) |
Necesitas isinstance() en tiempo de ejecución | Protocol con @runtime_checkable — pero mira la advertencia abajo |
| Atributo, no método | Los protocolos admiten miembros de datos: name: str en el cuerpo |
Notas para usuarios avanzados
@runtime_checkableverifica presencia, no firmas. La documentación advierte queisinstance()contra un protocolo runtime-checkable "verificará solo la presencia de los métodos o atributos requeridos, no sus firmas ni tipos". Un método con la firma equivocada igual pasa. Deja la verificación de firmas donde corresponde: en el checker estático.issubclass()solo funciona con protocolos sin datos. Según la especificación de tipado, los protocolos con miembros de atributo admitenisinstance()pero noissubclass().- Las verificaciones de protocolo en runtime son lentas. La documentación de la biblioteca estándar señala que un
isinstance()contra un protocolo runtime-checkable "puede ser sorprendentemente lento"; prefierehasattr()en rutas críticas. - Los miembros salen solo del cuerpo de la clase. Los atributos asignados vía
selfdentro de métodos no cuentan como miembros del protocolo — anótalos en el cuerpo. - Mantén los protocolos angostos. Un protocolo de un solo método definido junto a su consumidor (no junto a sus implementaciones) es la versión en Python del principio de segregación de interfaces — y es exactamente la costura que los agentes de codificación con AI simulan con más limpieza, porque el doble no necesita importar nada de tu módulo de producción.
Recursos
typing.Protocol— documentación de la biblioteca estándar de Python@runtime_checkable— advertencia de verificación solo por presencia- Protocols — especificación de tipado de Python
- PEP 544 — Protocols: Structural subtyping (static duck typing)
- Protocols and structural subtyping — documentación de mypy
Mira la versión de 60 segundos: reel #127 de la serie Yeda AI Tips. ¿Construyes flujos de ingeniería asistidos por AI? Yeda AI diseña, audita y entrega sistemas LLM de producción.