Protocolos y Tipado Estructural
typing.Protocol habilita el tipado pato estático: cualquier clase con métodos coincidentes satisface el protocolo sin necesidad de heredar de él. Este es el compañero en la capa de tipos para los patrones estructurales de OOP.
Receta
from typing import Protocol
class SupportsWrite(Protocol):
def write(self, data: bytes) -> int: ...
def persist(stream: SupportsWrite, payload: bytes) -> None:
stream.write(payload)Cuándo usar esto:
- Para tipar objetos similares a archivos y flujos (streams).
- Para definir puertos en arquitectura hexagonal.
- Para pruebas con fakes sin herencia de ABC.
- Para tipado gradual en código heredado con tipado pato.
Ejemplo Funcional
from typing import Protocol, runtime_checkable
@runtime_checkable
class Repository(Protocol):
def get(self, item_id: int) -> dict[str, object]: ...
def save(self, item: dict[str, object]) -> None: ...
class InMemoryRepo:
def __init__(self) -> None:
self._data: dict[int, dict[str, object]] = {}
def get(self, item_id: int) -> dict[str, object]:
return self._data[item_id]
def save(self, item: dict[str, object]) -> None:
self._data[int(item["id"])] = item
def service(repo: Repository) -> None:
repo.save({"id": 1, "name": "Ada"})
assert repo.get(1)["name"] == "Ada"
if __name__ == "__main__":
service(InMemoryRepo())
print(isinstance(InMemoryRepo(), Repository))Lo que esto demuestra:
InMemoryReposatisfaceRepositorysin herencia.- El servicio depende de una interfaz estructural.
@runtime_checkablehabilitaisinstanceen las pruebas.- Los métodos usan cuerpos con
...(elipsis) en la definición del Protocolo.
Inmersión Profunda
Cómo Funciona
- PEP 544 - Subtipado estructural para verificadores estáticos.
- Nominal vs. Estructural - ABC requiere herencia; Protocolo no.
- runtime_checkable -
isinstanceverifica solo los nombres de los métodos - superficial. - Herencia de Protocolo - Extiende protocolos para añadir métodos.
@propertyen Protocolo - Declara atributos de solo lectura estructuralmente.
Protocolo vs ABC
| Necesidad | Elegir |
|---|---|
| Coincidencia estructural con terceros | Protocolo |
| Forzar en la instanciación | ABC |
Validación profunda con isinstance | Ninguno solo - pruebas + tipos |
Notas de Python
class Readable(Protocol):
def read(self, n: int = -1) -> bytes: ...
# Protocolo genérico
from typing import TypeVar
T = TypeVar("T", contravariant=True)
class Box(Protocol[T]):
def put(self, item: T) -> None: ...Errores Comunes
- Falsa confianza con
isinstance-runtime_checkablees superficial. Solución: No depender de él para límites de seguridad. - Herencia en tiempo de ejecución de Protocolo - Solo para tipado; no esperes comportamiento de base de Protocolo en tiempo de ejecución.
- Métodos privados - Protocolo no puede ver métodos con name mangling. Solución: Solo métodos de interfaz pública.
- Deriva estructural - Cambios en la implementación rompen el verificador silenciosamente en los puntos de llamada. Solución: Ejecutar
mypyen CI en ambos lados. - Definiciones duplicadas de Protocolo - La equivalencia estructural no es automática entre módulos. Solución: Importar el tipo de Protocolo compartido.
Alternativas
| Alternativa | Usar Cuando | No Usar Cuando |
|---|---|---|
| ABC | Controlar el árbol de herencia | Tipos de terceros |
object | Sin ayuda estática | Necesidad de contratos de métodos |
| zope.interface | Ecosistema heredado | Código nuevo enfocado en tipos |
| clase concreta única | Una implementación | Múltiples formas |
Preguntas Frecuentes
¿Heredar de Protocolo?
Opcional para claridad de tipado; no es necesario para la coincidencia estructural en tiempo de ejecución.
¿Protocolo con propiedades?
Declara @property def x(self) -> int: ... en el cuerpo del protocolo.
¿Soporte de Protocolo en pyright?
Fuerte; a menudo más estricto que mypy en casos extremos de superposición de protocolos.
¿Protocolo para llamables?
Usa Protocol con __call__ o Callable; Callable es más simple solo para funciones.
¿total_match?
No está incorporado; asegúrate de que todos los métodos del protocolo estén implementados; los verificadores informan los faltantes.
¿Protocolo de array numpy?
Las bibliotecas definen Protocolos que coinciden con la API de tipo pato de ndarray sin importar numpy en stubs solo de tipos.
¿FastAPI Depends typing?
Depende de servicios tipados con Protocolo para dobles de prueba en funciones de ruta.
¿Protocolo congelado?
Los protocolos en sí mismos no imponen la inmutabilidad; documenta las expectativas de implementación.
¿typing_extensions?
3.14 tiene Protocol en la biblioteca estándar; versiones anteriores usaban el backport de typing_extensions.
¿Superposición con el artículo de OOP?
El artículo de OOP se enfoca en patrones de tiempo de ejecución; esta página se enfoca en la configuración del verificador estático y el diseño de API.
Relacionado
- Protocolos y Tipado Estructural - vista de OOP en tiempo de ejecución
- Clases Base Abstractas - alternativa nominal
- Genéricos y TypeVar - protocolos genéricos
- Configuración de mypy & pyright - configuración del verificador
Versiones de la pila: Esta página fue escrita para Python 3.14.0 (estable 3.14, mantenimiento 3.13), FastAPI 0.115+, Django 5.2, Flask 3.1, Pydantic 2, PyTorch 2.6+, pandas 2.2+, Polars 1.x, ruff 0.9+, y uv 0.6+.