typing.runtime_checkable permite usar determinados Protocol con isinstance() e issubclass(). Conecta tipado estructural e inspección de runtime, pero tiene una limitación importante: normalmente comprueba la presencia de miembros, no firmas completas, tipos de parámetros, retornos ni comportamiento.
Esta guía explica declaraciones, métodos, atributos, propiedades, falsos positivos, comparación con ABC, plugins, adaptadores, Protocol genéricos, rendimiento, validación más fuerte y cuándo conviene un registro explícito.
Protocol estructural
from typing import Protocol
class Guardable(Protocol):
def guardar(self) -> None: ...Una clase no necesita heredar de Guardable. Un analizador estático puede aceptar cualquier clase con un método compatible.
class Documento:
def guardar(self) -> None:
print("guardado")
def persistir(item: Guardable) -> None:
item.guardar()
persistir(Documento())La compatibilidad existe para el análisis estático. Un Protocol normal no puede usarse directamente en una comprobación de instancia.
Añadir runtime_checkable
from typing import Protocol, runtime_checkable
@runtime_checkable
class Guardable(Protocol):
def guardar(self) -> None: ...
print(isinstance(Documento(), Guardable))El decorador autoriza la inspección de runtime. El resultado es verdadero cuando el objeto expone los miembros requeridos de forma reconocible.
No valida firmas
class Incompatible:
def guardar(self, ruta: str, forzar: bool) -> int:
return 1
print(isinstance(Incompatible(), Guardable))El objeto posee un miembro invocable llamado guardar, por lo que el test puede pasar aunque la firma sea incompatible. El analizador estático puede detectar el problema; isinstance() no ofrece la misma precisión.
Presencia no significa comportamiento
Un objeto puede exponer el nombre correcto y aun así fallar, realizar otra acción o lanzar excepciones. runtime_checkable comprueba una forma superficial, no semántica. No trates el resultado como prueba de seguridad o corrección.
Atributos de datos
@runtime_checkable
class Nombrado(Protocol):
nombre: str
class Producto:
nombre = "Teclado"
isinstance(Producto(), Nombrado)El test busca un miembro nombre. No garantiza que el valor sea string. Un entero o descriptor problemático puede satisfacer la presencia.
Propiedades
@runtime_checkable
class ConTamano(Protocol):
@property
def tamano(self) -> int: ...Una propiedad o atributo con el nombre esperado puede pasar. El retorno y los efectos secundarios no se validan. Evita descriptores que realicen I/O o trabajo peligroso durante introspección.
Protocol solo de métodos e issubclass
issubclass() tiene restricciones mayores, especialmente con miembros de datos. Los Protocol compuestos solo por métodos son mejores para pruebas de clase.
@runtime_checkable
class Cerrable(Protocol):
def cerrar(self) -> None: ...
class Recurso:
def cerrar(self) -> None: ...
issubclass(Recurso, Cerrable)Comparación con hasattr
if hasattr(objeto, "guardar"):
objeto.guardar()hasattr() puede bastar para una comprobación local. runtime_checkable centraliza varias capacidades en un contrato reutilizable, documenta la interfaz y participa en el análisis estático.
Comparación con ABC
Una clase abstracta suele exigir herencia explícita o registro virtual. Es mejor cuando controlas la jerarquía, compartes implementación, impones métodos abstractos o necesitas identidad nominal. Protocol favorece desacoplamiento y compatibilidad con clases externas.
Plugins
@runtime_checkable
class Plugin(Protocol):
nombre: str
def iniciar(self) -> None: ...
def detener(self) -> None: ...
def cargar(candidato: object) -> Plugin:
if not isinstance(candidato, Plugin):
raise TypeError("plugin incompatible")
return candidatoEs un buen filtro inicial. Para plugins no confiables, valida versión, configuración, firmas, permisos y comportamiento. Puede ser necesario inspect.signature() o registro explícito.
Adaptadores
class AdaptadorArchivo:
def __init__(self, archivo) -> None:
self.archivo = archivo
def guardar(self) -> None:
self.archivo.flush()Cuando un objeto externo casi satisface el contrato, un adaptador es más seguro que confiar en una coincidencia accidental de nombres.
TypeGuard para comprobaciones más fuertes
from typing import TypeGuard
def es_guardable(valor: object) -> TypeGuard[Guardable]:
metodo = getattr(valor, "guardar", None)
return callable(metodo)Este ejemplo todavía comprueba solo invocabilidad, pero una función personalizada puede validar marcadores, versiones, atributos o firmas. Consulta TypeGuard en Python.
No llames métodos durante la validación
Invocar una capacidad para comprobarla puede causar efectos secundarios. Prefiere metadatos, introspección, registro y pruebas separadas. La validación de interfaz no debería enviar mensajes, escribir archivos ni alterar estado.
Rendimiento
Las pruebas de Protocol pueden ser más lentas que comprobaciones nominales simples porque inspeccionan miembros. No las repitas en bucles críticos. Valida una vez en la frontera y conserva la referencia tipada.
Caché y monkey-patching
Implementaciones modernas pueden congelar el conjunto de miembros y usar búsqueda estática. Modificar dinámicamente el Protocol o las clases candidatas puede producir resultados inesperados entre versiones.
Capacidades opcionales
Protocol no tiene miembros opcionales directos. Crea Protocol pequeños:
@runtime_checkable
class Guardable(Protocol):
def guardar(self) -> None: ...
@runtime_checkable
class Exportable(Protocol):
def exportar(self, ruta: str) -> None: ...El consumidor prueba solo la capacidad necesaria. La segregación mejora reutilización y reduce requisitos falsos.
Composición
class Repositorio(Guardable, Exportable, Protocol):
passUn Protocol compuesto reúne capacidades. Verifica comportamiento de runtime y decoradores en las versiones soportadas.
Protocol genérico
from typing import TypeVar
T = TypeVar("T")
@runtime_checkable
class Lector(Protocol[T]):
def leer(self) -> T: ...Los argumentos genéricos se borran en runtime. Puedes comprobar el Protocol no parametrizado, pero no confirmar que leer() devuelva un tipo específico.
Protocol parametrizado en isinstance
Evita isinstance(obj, Lector[str]). Los objetos de typing parametrizados normalmente no son clases de runtime adecuadas y el argumento genérico no puede verificarse.
Errores comunes
- Suponer que se validaron las firmas: normalmente solo se observan nombres.
- Usar el resultado como garantía de seguridad: comportamiento y valores no están probados.
- Comprobar Protocol genérico parametrizado: runtime no valida el argumento.
- Diseñar Protocol enormes: capacidades pequeñas son más reutilizables.
- Llamar métodos durante validación: pueden ocurrir efectos secundarios.
- Comprobar repetidamente en bucles: la introspección tiene coste.
Ejemplo completo: proveedores de notificación
from typing import Protocol, runtime_checkable
@runtime_checkable
class Notificador(Protocol):
nombre: str
def enviar(self, destino: str, mensaje: str) -> None: ...
class Email:
nombre = "email"
def enviar(self, destino: str, mensaje: str) -> None:
print(f"email a {destino}: {mensaje}")
class Sms:
nombre = "sms"
def enviar(self, destino: str, mensaje: str) -> None:
print(f"sms a {destino}: {mensaje}")
def registrar(candidato: object) -> Notificador:
if not isinstance(candidato, Notificador):
raise TypeError("notificador inválido")
return candidato
notificadores = [registrar(Email()), registrar(Sms())]
for notificador in notificadores:
notificador.enviar("cliente", "Pedido aprobado")La prueba acepta implementaciones sin herencia compartida. Un registro de producción también puede validar configuración, versión, límites y manejo de errores.
Cuándo evitar runtime_checkable
Usa herencia nominal cuando controlas todas las implementaciones y necesitas invariantes fuertes. Usa un validador explícito cuando firmas y valores importan en runtime. Usa registro de plugins cuando confianza y versionado son necesarios. Usa runtime_checkable cuando una comprobación superficial es suficiente para despacho o mejores errores.
Conclusión
runtime_checkable permite usar ciertos Protocol con isinstance() e issubclass(), facilitando plugins, adaptadores y detección de capacidades. La comprobación confirma principalmente presencia, no el contrato completo.
La documentación oficial de runtime_checkable en Python define las restricciones. Combina análisis estático con validación explícita cuando firmas, datos o seguridad sean importantes y trata la comprobación estructural como filtro, no como prueba absoluta.







