
Passo 1 de 8
Partir das necessidades do consumidor
Descubra o contrato mínimo observando o que o código consumidor realmente pede a seus colaboradores.
Trilha de aprendizado · Nível 12 · Tutorial 8
Formalizar contratos de colaboradores com Protocol e verificar implementações independentes sem exigir uma classe base compartilhada.
Partir das necessidades do consumidor
Descubra o contrato mínimo observando o que o código consumidor realmente pede a seus colaboradores. 2 min
Declarar as operações com Protocol
Declare um contrato estrutural mínimo para destinos que recebem mensagens, usando assinaturas anotadas e sem criar uma implementação. 3 min
Verificar classes no ponto de consumo
Use o protocolo onde o colaborador é recebido ou armazenado para que o mypy verifique a compatibilidade estrutural. 3 min
Corrigir divergências de assinatura
Use os diagnósticos do mypy para localizar e corrigir incompatibilidades entre as chamadas permitidas por um Protocol e os métodos oferecidos por implementações independentes. 4 min
Entender a exigência de atributos graváveis
Declare atributos em um Protocol e reconheça as obrigações de leitura e escrita que eles criam para cada implementação. 3 min
Exigir apenas leitura com property
Declare uma consulta somente de leitura no protocolo e preserve a liberdade de cada implementação para armazenar ou calcular o valor. 3 min
Aplicar o contrato à implementação e ao substituto de teste
Use o mesmo protocolo para uma implementação de console e um substituto que registra chamadas durante testes. 3 min
Consolidar uma interface estrutural verificável
Aplique o fluxo completo de Protocol em um caso integrado: contrato mínimo, assinaturas compatíveis, acesso somente de leitura e teste comportamental. 3 min

Passo 1 de 8
Descubra o contrato mínimo observando o que o código consumidor realmente pede a seus colaboradores.
Ao definir uma interface, comece pelo objeto que depende de outro: o consumidor. O contrato mínimo contém apenas as operações que esse consumidor realmente usa.
No caso condutor, ServicoDeAvisos recebe uma mensagem e a encaminha a um destino. Por enquanto, sua única necessidade é enviar essa mensagem.
class ServicoDeAvisos:
def __init__(self, destino) -> None:
self.destino = destino
def avisar(self, mensagem: str) -> None:
self.destino.enviar(mensagem)O serviço conhece apenas a operação de que precisa. As diferentes formas de entregar a mensagem permanecem atrás dessa fronteira.

O contrato mínimo, neste momento, é a capacidade de receber enviar(mensagem).
Dica
Não descreva o que uma implementação poderia fazer. Descreva somente o que o consumidor precisa chamar. Um contrato menor reduz acoplamento e amplia as opções de implementação.
class DestinoConsole:
def enviar(self, mensagem: str) -> None:
print(mensagem)
def configurar_cores(self, habilitadas: bool) -> None:
...
class DestinoEmail:
def enviar(self, mensagem: str) -> None:
print(f"E-mail enviado: {mensagem}")
def reconectar_servidor(self) -> None:
...As classes não precisam pertencer à mesma hierarquia para colaborar com o serviço. Ambas oferecem enviar, mas seus métodos adicionais são detalhes próprios.
ServicoDeAvisos não chama configurar_cores nem reconectar_servidor; portanto, essas operações não fazem parte de seu contrato.
Considerando exclusivamente o código de ServicoDeAvisos, qual deve ser a exigência mínima para um destino?

Passo 2 de 8
Declare um contrato estrutural mínimo para destinos que recebem mensagens, usando assinaturas anotadas e sem criar uma implementação.
No passo anterior, o serviço consumidor precisava apenas pedir que um destino enviasse uma mensagem. Agora vamos registrar essa necessidade em um contrato estrutural: qualquer objeto que ofereça a operação esperada poderá ser usado mais adiante.
Importe Protocol de typing e crie uma classe que herda dele. O nome descreve o papel exigido pelo consumidor, não uma implementação específica.
O protocolo fica entre quem consome a operação e quem a executa.

O consumidor depende do contrato mínimo; as classes concretas ficam separadas dele.
Crie o contrato em um arquivo Python:
from typing import Protocol
class DestinoMensagem(Protocol):
...Dentro do protocolo, declare o método que o consumidor pode chamar. Os tipos do parâmetro e do retorno fazem parte do contrato. Como este destino apenas realiza o envio, ele retorna None.
As reticências (...) substituem o corpo: elas declaram a operação, mas não dizem como enviar a mensagem.
O contrato completo, por enquanto, tem uma única operação:
from typing import Protocol
class DestinoMensagem(Protocol):
def enviar(self, mensagem: str) -> None:
...Exemplo
DestinoMensagem afirma que o consumidor poderá chamar enviar com uma str e não receberá um valor de volta. Ele não imprime nada, não envia e-mail e não armazena mensagens: essas ações pertencem a classes concretas, declaradas separadamente.
Atenção
As anotações permitem que um verificador estático, como o mypy, analise o código. Definir DestinoMensagem não faz Python validar objetos automaticamente quando o programa está em execução e não cria uma implementação para enviar.
No próximo passo, o consumidor será anotado com esse protocolo. Nesse ponto, o mypy poderá verificar se um objeto independente oferece a operação exigida — sem que a classe precise herdar explicitamente de DestinoMensagem.
Complete a declaração:
from typing import Protocol
class DestinoMensagem(____):
def enviar(self, mensagem: str) -> None:
...Complete o corpo do método no protocolo:
class DestinoMensagem(Protocol):
def enviar(self, mensagem: str) -> None:
____
Passo 3 de 8
Use o protocolo onde o colaborador é recebido ou armazenado para que o mypy verifique a compatibilidade estrutural.
Depois de declarar um Protocol, use-o no ponto em que um objeto é recebido. Assim, o serviço depende do contrato DestinoMensagem, e não de uma classe concreta.
O mypy verifica cada valor passado para encaminhar: ele precisa oferecer enviar(mensagem: str) -> None. Não precisa herdar do protocolo.
from typing import Protocol
class DestinoMensagem(Protocol):
def enviar(self, mensagem: str) -> None: ...
def encaminhar(destino: DestinoMensagem, texto: str) -> None:
destino.enviar(texto)
class Console:
def enviar(self, mensagem: str) -> None:
print(mensagem)
encaminhar(Console(), "Pedido recebido")
A anotação do parâmetro estabelece o contrato usado pelo consumidor; Console é compatível pela estrutura, sem herança.
Anote uma variável quando ela guardar um colaborador e um atributo quando a classe mantiver essa dependência. Em cada atribuição, o mypy confere se o valor é compatível com o protocolo.
Dentro de ServicoAvisos, o atributo tem tipo DestinoMensagem. Portanto, o código do serviço só pode usar membros prometidos por esse contrato.
class ServicoAvisos:
def __init__(self, destino: DestinoMensagem) -> None:
self.destino: DestinoMensagem = destino
def avisar(self, texto: str) -> None:
self.destino.enviar(texto)
class Email:
def enviar(self, mensagem: str) -> None:
print(f"E-mail: {mensagem}")
def conectar_smtp(self) -> None:
print("Conectando...")
principal: DestinoMensagem = Console()
servico = ServicoAvisos(Email())
servico.avisar("Pagamento aprovado")Email tem conectar_smtp, mas esse método não está no protocolo. A compatibilidade permite usar Email como destino porque ele fornece enviar; porém, dentro de ServicoAvisos, self.destino.conectar_smtp() é um erro de tipo.
Isso preserva o desacoplamento: o consumidor continua válido para qualquer implementação que cumpra o contrato mínimo.
Relacione cada trecho ao que o mypy verifica.
Toque em um item e depois no par correspondente.

Passo 4 de 8
Use os diagnósticos do mypy para localizar e corrigir incompatibilidades entre as chamadas permitidas por um Protocol e os métodos oferecidos por implementações independentes.
Uma classe pode ter um método chamado enviar e ainda assim não cumprir o protocolo. A implementação precisa aceitar todas as entradas que o consumidor pode fazer pelo contrato e devolver um resultado compatível.
O mypy compara a assinatura usada no ponto de consumo com a assinatura concreta. Assim, ele pode apontar erro mesmo quando o método existe.
Compare as chamadas que o protocolo autoriza com as que a implementação realmente aceita.

Se um slot aceito pelo contrato falta, muda de forma ou produz outro tipo de retorno, a implementação não é substituível.
Exemplo
from typing import Protocol
class Destino(Protocol):
def enviar(self, mensagem: str, *, urgente: bool = False) -> bool: ...
class Console:
def enviar(self, texto: str, urgente: bool) -> None:
print(texto)
def encaminhar(destino: Destino, mensagem: str) -> bool:
return destino.enviar(mensagem=mensagem)
encaminhar(Console(), "Relatório pronto") # erro do mypyConsole.enviar diverge em três pontos: o parâmetro se chama texto, urgente virou obrigatório e o retorno é None, não bool.
Como o consumidor pode chamar enviar(mensagem=...), o nome mensagem faz parte da compatibilidade. Como ele pode omitir urgente, a implementação não pode exigir esse argumento.
Além disso, se o consumidor recebe um bool, a implementação deve produzir um bool. Não enfraqueça o protocolo nem use Any para esconder a divergência: corrija a assinatura que não atende ao contrato.
A assinatura abaixo preserva exatamente as necessidades do consumidor.
from typing import Protocol
class Destino(Protocol):
def enviar(self, mensagem: str, *, urgente: bool = False) -> bool: ...
class Console:
def enviar(self, mensagem: str, *, urgente: bool = False) -> bool:
prefixo = "[URGENTE] " if urgente else ""
print(f"{prefixo}{mensagem}")
return True
def encaminhar(destino: Destino, mensagem: str) -> bool:
return destino.enviar(mensagem=mensagem)
encaminhar(Console(), "Relatório pronto")
# mypy aceita Console como Destino, sem herança explícita.Dica
Pergunte: “Toda chamada válida para o Protocol também funciona nesta implementação?”. Parâmetros obrigatórios extras e parâmetros com nomes incompatíveis para chamadas nomeadas fazem a resposta ser não.
Associe cada trecho de implementação ao motivo pelo qual ele não atende ao contrato enviar(self, mensagem: str, *, urgente: bool = False) -> bool.
Toque em um item e depois no par correspondente.
Uma implementação foi escrita assim:
class Arquivo:
def enviar(self, mensagem: str, prioridade: int) -> None:
print(mensagem)Sem mudar o Protocol enviar(self, mensagem: str, *, urgente: bool = False) -> bool, escreva uma assinatura compatível para Arquivo.enviar e explique por que prioridade não pode continuar como parâmetro obrigatório.
Escreva pelo menos 80 caracteres (0/80).

Passo 5 de 8
Declare atributos em um Protocol e reconheça as obrigações de leitura e escrita que eles criam para cada implementação.
Além de enviar mensagens, o serviço agora precisa consultar e alterar o identificador do destino. Ao declarar destino: object no corpo do Protocol, você afirma que qualquer colaborador compatível deve expor um atributo que possa ser lido e também receber atribuições de valores do tipo object.
Essa exigência é mais forte do que apenas ter um atributo com esse nome: o consumidor pode executar tanto self._destino.destino quanto self._destino.destino = novo_destino.
O mesmo atributo participa de duas operações permitidas pelo contrato.

Um atributo declarado diretamente no protocolo precisa suportar a leitura e a escrita usadas pelo consumidor.
O tipo object permite ao consumidor atribuir qualquer objeto ao identificador do destino.
from typing import Protocol
class DestinoDeMensagens(Protocol):
destino: object
def enviar(self, mensagem: str) -> None: ...
class Notificador:
def __init__(self, destino: DestinoDeMensagens) -> None:
self._destino = destino
def identificar_destino(self) -> object:
return self._destino.destino
def redirecionar(self, novo_destino: object) -> None:
self._destino.destino = novo_destino
def avisar(self, mensagem: str) -> None:
self._destino.enviar(mensagem)Se o contrato permite escrever qualquer object, uma implementação com destino: str não é segura. O Notificador poderia receber, por exemplo, um objeto de configuração e atribuí-lo ao campo; isso violaria a promessa da implementação de armazenar apenas str.
Do mesmo modo, uma propriedade sem setter pode ser lida, mas não aceita a atribuição que o consumidor está autorizado a fazer. Portanto, ela não satisfaz este contrato gravável.
O mypy aponta o problema quando uma dessas instâncias é usada onde DestinoDeMensagens é esperado.
class CanalTexto:
def __init__(self, destino: str) -> None:
self.destino = destino
def enviar(self, mensagem: str) -> None:
print(f"{self.destino}: {mensagem}")
class CanalSemSetter:
def __init__(self, destino: object) -> None:
self._destino = destino
@property
def destino(self) -> object:
return self._destino
def enviar(self, mensagem: str) -> None:
print(mensagem)
# Ambos são incompatíveis com o parâmetro de Notificador:
# Notificador(CanalTexto("console"))
# Notificador(CanalSemSetter("console"))Exemplo
Uma implementação compatível pode armazenar object no atributo e oferecer o método exigido:
class CanalConfiguravel:
def __init__(self, destino: object) -> None:
self.destino = destino
def enviar(self, mensagem: str) -> None:
print(f"{self.destino}: {mensagem}")
notificador = Notificador(CanalConfiguravel("console"))
notificador.redirecionar({"fila": "avisos"})Aqui, a atribuição de um dicionário é permitida pelo contrato e pela implementação.
Atenção
O Protocol e o mypy verificam se as operações são compatíveis estaticamente. Eles não verificam, durante a execução, se um objeto escolhido como destino faz sentido para a regra de negócio.
Considerando o protocolo DestinoDeMensagens com destino: object e enviar(self, mensagem: str) -> None, qual classe pode ser passada para Notificador?

Passo 6 de 8
Declare uma consulta somente de leitura no protocolo e preserve a liberdade de cada implementação para armazenar ou calcular o valor.
No step anterior, nome: str no corpo de um Protocol exigia leitura e escrita: o consumidor poderia fazer destino.nome = "novo".
Se o consumidor só precisa consultar o nome para exibi-lo, declare uma propriedade sem setter. Assim, a interface oferece leitura, mas não autoriza atribuições por meio da referência tipada pelo protocolo.
A propriedade delimita o que o consumidor pode fazer, independentemente das capacidades do objeto concreto.

O consumidor lê nome; ele não recebe permissão para alterá-lo pelo contrato.
A reticência declara a operação exigida, sem implementar seu corpo.
from typing import Literal, Protocol
class DestinoDeMensagem(Protocol):
@property
def nome(self) -> str: ...
def enviar(self, mensagem: str) -> None: ...
class DestinoConsole:
def __init__(self, nome: str) -> None:
self.nome = nome # atributo comum e mutável no objeto concreto
def enviar(self, mensagem: str) -> None:
print(f"[{self.nome}] {mensagem}")
class DestinoAuditoria:
@property
def nome(self) -> Literal["auditoria"]:
return "auditoria"
def enviar(self, mensagem: str) -> None:
print(f"AUDIT: {mensagem}")
def rotulo(destino: DestinoDeMensagem) -> str:
return f"Destino: {destino.nome}"
console = DestinoConsole("terminal")
print(rotulo(console))
console.nome = "terminal-secundario" # permitido: console é concreto
contrato: DestinoDeMensagem = console
# contrato.nome = "outro" # erro do mypy: a propriedade do Protocol não tem setter
Se uma classe compatível possui um atributo nome mutável, esse atributo se torna imutável no objeto concreto quando ele é usado por um protocolo com @property sem setter.
Um serviço só exibe o nome do destino antes de enviar uma mensagem; ele nunca renomeia esse destino. Qual declaração deve entrar no Protocol para nome? Explique também por que uma implementação com atributo nome mutável ainda pode ser compatível, sem liberar atribuição pelo contrato.
Escreva pelo menos 120 caracteres (0/120).

Passo 7 de 8
Use o mesmo protocolo para uma implementação de console e um substituto que registra chamadas durante testes.
O serviço de mensagens conhece apenas o contrato Destino: ele consulta identificador e chama enviar(). Por isso, tanto um destino que imprime no console quanto outro que guarda mensagens em memória podem ser usados sem herdar de Destino.
A implementação em memória possui mensagens, mas esse detalhe é útil somente ao teste. Ele não pertence ao protocolo porque o consumidor não precisa dele.
O diagrama separa o que o mypy verifica do que o teste verifica.

O protocolo limita o serviço a identificador e enviar(). O teste conserva a referência concreta para consultar mensagens.
Crie este arquivo completo. A anotação Destino no construtor é o ponto de consumo comum.
from typing import Protocol
class Destino(Protocol):
@property
def identificador(self) -> str: ...
def enviar(self, mensagem: str) -> None: ...
class ConsoleDestino:
def __init__(self, identificador: str) -> None:
self._identificador = identificador
@property
def identificador(self) -> str:
return self._identificador
def enviar(self, mensagem: str) -> None:
print(f"[{self.identificador}] {mensagem}")
class DestinoMemoria:
def __init__(self, identificador: str) -> None:
self.identificador = identificador
self.mensagens: list[str] = []
def enviar(self, mensagem: str) -> None:
self.mensagens.append(mensagem)
class ServicoMensagens:
def __init__(self, destino: Destino) -> None:
self._destino = destino
def encaminhar(self, mensagem: str) -> None:
self._destino.enviar(mensagem)
# Esta atribuição faz o mypy verificar ConsoleDestino contra Destino.
destino_console: Destino = ConsoleDestino("console")
servico_console = ServicoMensagens(destino_console)
Crie este segundo arquivo na mesma pasta. O teste passa o substituto ao serviço, mas mantém destino com seu tipo concreto para inspecionar os registros.
from mensagens import DestinoMemoria, ServicoMensagens
def test_encaminha_para_o_destino_em_memoria() -> None:
destino = DestinoMemoria("teste")
servico = ServicoMensagens(destino)
servico.encaminhar("Pedido recebido")
assert destino.identificador == "teste"
assert destino.mensagens == ["Pedido recebido"]
No terminal, dentro da pasta dos arquivos, execute:
python -m mypy --strict mensagens.py test_mensagens.py
python -m pytest -qO mypy verifica se as classes e as chamadas usadas satisfazem as anotações. O pytest executa o cenário e comprova que, neste caso, a mensagem foi registrada.
Atenção
O mypy não executa enviar() nem confirma que a mensagem foi armazenada. Por outro lado, um teste que passa não substitui a análise estática de outros usos possíveis do contrato. Use as duas evidências: compatibilidade estrutural e comportamento observado.
Depois de executar os dois comandos, relate o resultado de cada ferramenta. Indique qual delas fornece evidência de compatibilidade com Destino e qual confirma o registro efetivo da mensagem.
Escreva pelo menos 80 caracteres (0/80).

Passo 8 de 8
Aplique o fluxo completo de Protocol em um caso integrado: contrato mínimo, assinaturas compatíveis, acesso somente de leitura e teste comportamental.
Comece pelo que o consumidor realmente faz. Se ele encaminha uma mensagem e apenas consulta o nome do destino, o contrato deve expor somente essas duas operações:
deliver(...), com a assinatura que o consumidor pode chamar;name, somente para leitura.Em seguida, anote o ponto de consumo com o protocolo e deixe o mypy verificar cada implementação independente. Membros extras — como uma lista de registros usada no teste — permanecem fora do contrato quando o consumidor não precisa deles.
O protocolo fica entre o consumidor e implementações que não precisam compartilhar uma classe base.

O consumidor enxerga apenas deliver e name; cada implementação pode ter detalhes próprios.
Copie o código para um arquivo, por exemplo destinos.py. Complete o protocolo e corrija somente o que for necessário na assinatura de ConsoleDestination.
Repare que MessageService lê destination.name, mas nunca atribui a esse atributo. Portanto, o contrato não deve exigir escrita. A chamada urgent=True também precisa ser aceita por toda implementação compatível.
Substitua os dois ... do protocolo e ajuste o método marcado com # CORRIGIR. Depois execute python -m mypy --strict destinos.py e python destinos.py.
from typing import Protocol
class Destination(Protocol):
# Declare name como uma consulta somente de leitura.
...
# Declare a operação usada pelo consumidor.
...
class MessageService:
def __init__(self, destination: Destination) -> None:
self.destination = destination
def notify(self, message: str) -> None:
print(f"Enviando para {self.destination.name}")
self.destination.deliver(message, urgent=True)
class ConsoleDestination:
def __init__(self, name: str) -> None:
self._name = name
@property
def name(self) -> str:
return self._name
# CORRIGIR: a assinatura atual não atende ao contrato.
def deliver(self, message: str, channel: str) -> None:
print(f"[{channel}] {message}")
class MemoryDestination:
def __init__(self, name: str) -> None:
self.name = name
self.records: list[tuple[str, bool]] = []
def deliver(self, message: str, *, urgent: bool = False) -> None:
self.records.append((message, urgent))
memory = MemoryDestination("teste")
service = MessageService(memory)
service.notify("Backup concluído")
assert memory.records == [("Backup concluído", True)]
print("Teste comportamental passou")Relate: (1) como declarou name; (2) qual assinatura final usou em deliver; (3) por que ConsoleDestination e MemoryDestination não precisam herdar de Destination; e (4) o que foram capazes de evidenciar, separadamente, o mypy e o assert.
Escreva pelo menos 180 caracteres (0/180).
Resumo
Use este checklist ao definir um novo Protocol.
@property sem setter.records do substituto de teste.Any nem imponha herança quando ela não é necessária.Parabéns! Você concluiu: Definir interfaces estruturais com Protocol
50 XP
Milhares de cursos online em vídeo, ebooks e áudiobooks.
Para testar seus conhecimentos no decorrer dos cursos online
Gerado diretamente na galeria de fotos do seu celular e enviado ao seu e-mail
Baixe nosso aplicativo pelo QR Code ou pelos links abaixo:.
+ de 10 milhões
de alunos
Certificado grátis e
válido em todo o Brasil
60 mil exercícios
gratuitos
4,8/5 classificação
nas lojas de apps
Cursos gratuitos em
vídeo, ebooks e audiobooks