Em projetos Python grandes, muitas funções precisam aceitar objetos com determinado comportamento sem exigir que todos herdem da mesma classe. Um serviço pode precisar apenas de um método save(); um logger, de write(); um cache, de get() e set(). O typing.Protocol permite descrever essas capacidades com tipagem estrutural: se um objeto possui os membros esperados, ele é compatível para ferramentas de análise estática, mesmo sem herança explícita.
Neste guia, você aprenderá a criar protocolos, usar atributos e métodos, definir protocolos genéricos, combinar com runtime_checkable, testar implementações, trabalhar com callbacks e entender quando uma classe abstrata ainda é a melhor escolha.
Tipagem nominal e tipagem estrutural
Na tipagem nominal, a compatibilidade depende do nome e da hierarquia. Uma classe implementa uma interface porque herda dela. Na tipagem estrutural, a compatibilidade depende da forma: os métodos e atributos presentes.
from typing import Protocol
class Gravavel(Protocol):
def salvar(self, destino: str) -> None:
...
class Relatorio:
def salvar(self, destino: str) -> None:
print(f"Salvando em {destino}")
def persistir(item: Gravavel) -> None:
item.salvar("saida.txt")
persistir(Relatorio())Relatorio não herda de Gravavel, mas possui a assinatura exigida. Um verificador como mypy ou Pyright reconhece a compatibilidade.
Por que Protocol é útil?
Protocolos reduzem acoplamento. A função declara apenas o comportamento de que precisa, em vez de depender de uma implementação concreta. Isso facilita substituir bancos, clientes HTTP, caches e serviços em testes.
O conceito complementa o guia sobre type hints em Python. Type hints tornam contratos visíveis; Protocol permite que esses contratos sejam pequenos e orientados ao uso.
Protocolos com atributos
Um protocolo pode exigir atributos, propriedades e métodos.
from typing import Protocol
class UsuarioVisivel(Protocol):
id: int
nome: str
@property
def ativo(self) -> bool:
...
def exibir(usuario: UsuarioVisivel) -> str:
estado = "ativo" if usuario.ativo else "inativo"
return f"{usuario.id}: {usuario.nome} ({estado})"Tenha cuidado com atributos mutáveis. Se o protocolo declara valor: int, ele sugere leitura e escrita. Quando apenas leitura é necessária, uma propriedade sem setter costuma expressar melhor o contrato.
Métodos com assinaturas precisas
Parâmetros, tipos de retorno e nomes relevantes precisam ser compatíveis. Um método que retorna str não satisfaz um protocolo que exige bytes. Também é importante respeitar regras de variância: uma implementação não pode aceitar entradas mais restritas do que o contrato promete.
class Serializador(Protocol):
def dumps(self, objeto: object, *, identar: bool = False) -> str:
...Parâmetros somente nomeados, posicionais e opcionais fazem parte da interface. Defina apenas aquilo que o consumidor realmente usa.
Protocolos genéricos
Quando o tipo processado deve ser preservado, use parâmetros genéricos.
from typing import Protocol, TypeVar
T = TypeVar("T")
class Repositorio(Protocol[T]):
def obter(self, id: int) -> T | None:
...
def adicionar(self, item: T) -> None:
...
class Produto:
def __init__(self, nome: str) -> None:
self.nome = nome
def buscar_produto(repo: Repositorio[Produto], id: int) -> Produto:
produto = repo.obter(id)
if produto is None:
raise LookupError(id)
return produtoO verificador mantém a relação entre o repositório e o tipo retornado, evitando casts espalhados.
Protocolos para callbacks
Callable funciona para funções simples, mas um protocolo de callback pode representar parâmetros nomeados, overloads e atributos adicionais.
class AoConcluir(Protocol):
def __call__(self, resultado: str, *, duracao: float) -> None:
...
def executar(callback: AoConcluir) -> None:
callback("ok", duracao=0.42)Qualquer função ou objeto chamável com assinatura compatível pode ser passado.
runtime_checkable
Protocolos existem principalmente para análise estática. Com @runtime_checkable, é possível usar isinstance() para verificar a presença superficial de membros.
from typing import Protocol, runtime_checkable
@runtime_checkable
class Fechavel(Protocol):
def close(self) -> None:
...
if isinstance(recurso, Fechavel):
recurso.close()A verificação em runtime não valida assinaturas detalhadas, tipos de retorno ou semântica. Ela confirma apenas que os atributos necessários existem. Não trate isso como validação completa de interface.
Protocol versus ABC
Uma classe abstrata, explicada no artigo sobre abc no Python, é útil quando você controla as implementações, precisa compartilhar código, registrar subclasses ou impedir instanciação incompleta. Protocol é melhor quando deseja aceitar objetos de terceiros e aplicar duck typing com apoio estático.
Os dois mecanismos podem coexistir. Uma biblioteca pode fornecer uma ABC para implementações oficiais e expor um Protocol menor para consumidores.
Dependências e testes
Protocolos são especialmente úteis na injeção de dependências.
class EnviadorEmail(Protocol):
def enviar(self, destino: str, assunto: str, corpo: str) -> None:
...
class ServicoCadastro:
def __init__(self, email: EnviadorEmail) -> None:
self.email = email
def cadastrar(self, endereco: str) -> None:
# persistência omitida
self.email.enviar(endereco, "Bem-vindo", "Conta criada")
class EmailFake:
def __init__(self) -> None:
self.mensagens: list[tuple[str, str, str]] = []
def enviar(self, destino: str, assunto: str, corpo: str) -> None:
self.mensagens.append((destino, assunto, corpo))O teste usa EmailFake sem herdar de uma base de produção. A compatibilidade é verificada pelas assinaturas.
Protocolos recursivos e composição
Protocolos podem referenciar a si mesmos e herdar de outros protocolos.
class Nomeado(Protocol):
nome: str
class NoArvore(Nomeado, Protocol):
@property
def filhos(self) -> list["NoArvore"]:
...Prefira compor contratos pequenos. Um protocolo gigantesco recria o acoplamento que a tipagem estrutural deveria evitar.
Erros comuns
- Declarar membros demais: inclua somente o que a função consome.
- Usar runtime_checkable como validação: ele não confere assinaturas.
- Confundir atributo mutável com propriedade: isso pode gerar incompatibilidades.
- Ignorar ferramentas estáticas: Protocol não impõe regras sozinho durante a execução.
- Criar protocolo para uma única implementação sem necessidade: abstrações devem resolver um problema real.
- Exigir herança explícita: isso elimina a principal vantagem estrutural.
Boas práticas
Nomeie protocolos por capacidade, como Legivel, Fechavel ou Repositorio. Coloque o contrato próximo do código consumidor, pois é o consumidor que sabe quais operações precisa. Execute mypy ou Pyright no CI e adicione testes comportamentais quando a semântica for importante.
Em APIs públicas, documente também expectativas que os tipos não expressam, como idempotência, ordem, thread safety e tratamento de erros.
Exemplo completo: cache substituível
from typing import Protocol, TypeVar
T = TypeVar("T")
class Cache(Protocol[T]):
def get(self, chave: str) -> T | None:
...
def set(self, chave: str, valor: T, ttl: int) -> None:
...
class CacheMemoria:
def __init__(self) -> None:
self._dados: dict[str, object] = {}
def get(self, chave: str):
return self._dados.get(chave)
def set(self, chave: str, valor: object, ttl: int) -> None:
self._dados[chave] = valor
def carregar(cache: Cache[str], chave: str) -> str:
valor = cache.get(chave)
if valor is None:
valor = "calculado"
cache.set(chave, valor, ttl=60)
return valorUma implementação Redis, um fake de teste ou um adaptador de biblioteca externa pode satisfazer o mesmo contrato sem depender da classe CacheMemoria.
Conclusão
typing.Protocol adiciona verificação estática ao duck typing tradicional do Python. Ele permite definir contratos pequenos, aceitar implementações independentes e reduzir acoplamento entre camadas.
A documentação oficial de Protocol no módulo typing detalha herança, generics e checagem em runtime. Use protocolos quando o comportamento importa mais do que a árvore de classes e mantenha cada contrato mínimo, preciso e orientado ao consumidor.







