typing.runtime_checkable permite que determinados Protocols sejam usados com isinstance() e issubclass(). O recurso conecta tipagem estrutural e inspeção de runtime, mas com uma limitação importante: a verificação normalmente confirma apenas a presença dos membros, não suas assinaturas completas, tipos de parâmetros ou comportamento.
Neste guia, você aprenderá a declarar Protocols verificáveis, entender o que o teste realmente garante, trabalhar com métodos, atributos e propriedades, evitar falsos positivos, comparar Protocol com ABC, projetar plugins e adaptadores e decidir quando uma validação explícita é mais segura.
Protocol estrutural
from typing import Protocol
class Gravavel(Protocol):
def salvar(self) -> None: ...Uma classe não precisa herdar de Gravavel. Se possuir um método compatível, o analisador pode tratá-la como implementação estrutural.
class Documento:
def salvar(self) -> None:
print("salvo")
def persistir(item: Gravavel) -> None:
item.salvar()
persistir(Documento())Essa compatibilidade existe para o verificador estático. Um Protocol comum não pode ser usado diretamente com isinstance().
Adicionando runtime_checkable
from typing import Protocol, runtime_checkable
@runtime_checkable
class Gravavel(Protocol):
def salvar(self) -> None: ...
print(isinstance(Documento(), Gravavel))O decorador autoriza a inspeção em runtime. O resultado será verdadeiro quando o objeto expuser os membros exigidos de forma reconhecível.
O teste não valida a assinatura
class Incompativel:
def salvar(self, caminho: str, forcar: bool) -> int:
return 1
print(isinstance(Incompativel(), Gravavel))A classe possui um atributo chamável chamado salvar, então o teste pode retornar verdadeiro mesmo que a assinatura não seja compatível com o Protocol. O analisador estático pode detectar a incompatibilidade; isinstance() não oferece a mesma precisão.
Presença não significa comportamento
Um objeto pode possuir o método e ainda falhar, fazer outra coisa ou lançar exceções. runtime_checkable verifica forma superficial, não semântica. Nunca trate o resultado como prova de qualidade, segurança ou correção funcional.
Atributos de dados
@runtime_checkable
class Nomeado(Protocol):
nome: str
class Produto:
nome = "Teclado"
isinstance(Produto(), Nomeado)O teste procura o membro nome. Ele não garante que o valor seja string. Um inteiro ou propriedade problemática pode passar pela inspeção superficial.
Propriedades
@runtime_checkable
class ComTamanho(Protocol):
@property
def tamanho(self) -> int: ...A presença de uma propriedade ou atributo com o nome esperado pode satisfazer o teste. O tipo de retorno e possíveis efeitos colaterais não são verificados. Evite propriedades que executam I/O ou lógica perigosa apenas para responder à introspecção.
Protocol somente de métodos e issubclass
issubclass() possui restrições maiores, especialmente quando o Protocol contém membros de dados. Protocols compostos apenas por métodos são mais adequados para testes de classe.
@runtime_checkable
class Fechavel(Protocol):
def fechar(self) -> None: ...
class Recurso:
def fechar(self) -> None: ...
issubclass(Recurso, Fechavel)Comparação com hasattr
if hasattr(objeto, "salvar"):
objeto.salvar()hasattr() testa apenas um nome e pode ser suficiente em código local. runtime_checkable centraliza um conjunto de membros em um contrato reutilizável, melhora documentação e se integra ao analisador estático.
Comparação com ABC
Uma classe abstrata exige herança explícita ou registro virtual. Protocol aceita implementação estrutural. ABC é melhor quando você controla a hierarquia, precisa compartilhar implementação, impor métodos abstratos durante construção ou manter identidade nominal. Protocol é melhor para desacoplamento e compatibilidade com classes externas.
Plugins
@runtime_checkable
class Plugin(Protocol):
nome: str
def iniciar(self) -> None: ...
def parar(self) -> None: ...
def carregar(objeto: object) -> Plugin:
if not isinstance(objeto, Plugin):
raise TypeError("plugin incompatível")
return objetoEsse teste funciona como triagem inicial. Para plugins não confiáveis, valide tipos, assinaturas, versão da API e comportamento necessário. Considere usar inspect.signature() ou uma fase explícita de registro.
Adaptadores
Quando uma biblioteca externa quase satisfaz o Protocol, um adaptador pode oferecer a interface correta:
class AdaptadorArquivo:
def __init__(self, arquivo) -> None:
self.arquivo = arquivo
def salvar(self) -> None:
self.arquivo.flush()Adaptadores são mais seguros do que confiar em uma coincidência superficial de nomes.
TypeGuard para validação mais forte
from typing import TypeGuard
def eh_gravavel(valor: object) -> TypeGuard[Gravavel]:
metodo = getattr(valor, "salvar", None)
return callable(metodo)Esse TypeGuard ainda verifica apenas chamabilidade, mas permite adicionar regras específicas. Você pode inspecionar outros atributos, versões ou marcadores. O guia sobre TypeGuard no Python explica a relação de confiança.
Não chame membros durante a validação
Executar o método para descobrir se ele funciona pode causar efeitos colaterais. Prefira inspeção, metadados, registro explícito e testes separados. A validação de interface não deve salvar arquivos, enviar mensagens ou alterar estado.
Desempenho
Testes com Protocol podem ser mais lentos que verificações nominais simples porque precisam inspecionar membros. Não use isinstance(obj, Protocolo) repetidamente em loops críticos. Valide uma vez na fronteira e mantenha a referência tipada.
Cache e mudanças de classe
Implementações modernas podem observar membros definidos na criação do Protocol e usar inspeção estática. Alterar dinamicamente atributos do Protocol ou monkey-patching de classes pode produzir resultados inesperados entre versões. Evite depender de mutação dinâmica da interface.
Métodos opcionais
Protocol não possui conceito direto de membro opcional. Se uma capacidade é opcional, crie Protocols menores:
@runtime_checkable
class Salvavel(Protocol):
def salvar(self) -> None: ...
@runtime_checkable
class Exportavel(Protocol):
def exportar(self, caminho: str) -> None: ...O consumidor testa apenas a capacidade necessária. Essa segregação melhora design e reduz falsos requisitos.
Composição de capacidades
class Repositorio(Salvavel, Exportavel, Protocol):
passUm Protocol composto reúne capacidades. Para runtime, confirme se a composição e os decoradores são suportados como esperado pelo interpretador e pelo analisador.
Protocol genérico
from typing import TypeVar
T = TypeVar("T")
@runtime_checkable
class Leitor(Protocol[T]):
def ler(self) -> T: ...Em runtime, parâmetros genéricos são apagados. Você pode testar o Protocol não parametrizado, mas não confirmar que ler() retorna um tipo específico apenas com isinstance().
Protocol parametrizado em isinstance
Evite testes como isinstance(obj, Leitor[str]). Tipos parametrizados geralmente não são classes adequadas para esse uso. A inspeção de runtime não conhece o argumento genérico.
Erros comuns
- Assumir que assinaturas foram verificadas: normalmente só os nomes são observados.
- Usar o teste como validação de segurança: comportamento e tipos reais não são garantidos.
- Testar Protocol genérico parametrizado: argumentos de tipo não existem dessa forma em runtime.
- Criar Protocols enormes: pequenas capacidades são mais reutilizáveis.
- Executar métodos durante validação: pode causar efeitos colaterais.
- Usar em loops críticos: a introspecção pode ter custo relevante.
Exemplo completo: sistema de notificações
from typing import Protocol, runtime_checkable
@runtime_checkable
class Notificador(Protocol):
nome: str
def enviar(self, destino: str, mensagem: str) -> None: ...
class Email:
nome = "email"
def enviar(self, destino: str, mensagem: str) -> None:
print(f"email para {destino}: {mensagem}")
class Sms:
nome = "sms"
def enviar(self, destino: str, mensagem: str) -> None:
print(f"sms para {destino}: {mensagem}")
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 aprovado")O teste aceita implementações sem herança comum. Em produção, o registro também pode validar configuração, versão, limites e tratamento de erros.
Quando evitar runtime_checkable
Use herança nominal quando você controla todas as classes e precisa de invariantes fortes. Use uma função explícita de validação quando assinaturas e valores importam em runtime. Use registro de plugins quando confiança e versionamento são necessários. Use runtime_checkable quando uma verificação estrutural superficial é suficiente para escolher um caminho ou emitir uma mensagem de erro melhor.
Conclusão
runtime_checkable torna Protocols utilizáveis em isinstance() e issubclass(), facilitando plugins, adaptadores e detecção de capacidades. Porém, o teste confirma principalmente a presença dos membros, não o contrato completo.
A documentação oficial de runtime_checkable no módulo typing detalha as restrições. Combine análise estática com validação explícita quando assinaturas, dados ou segurança forem importantes, e trate o teste estrutural como uma triagem, não como uma prova absoluta.







