Protocol no Python: tipagem estrutural

Publicado em: 28/08/2026
Tempo de leitura: 5 minutos
A developer typing code on a laptop with a Python book beside in an office.

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 produto

O 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 valor

Uma 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.

Compartilhe:

Facebook
WhatsApp
Twitter
LinkedIn

Conteúdo do artigo

    Artigos relacionados

    A close-up shot showcasing the intricate scales of a snake, highlighting texture and color.
    Python Avançado
    Foto de perfil de Leandro Hirt da Academify

    memoryview no Python: buffers sem cópia

    Aprenda memoryview no Python para acessar buffers sem cópia, criar slices, editar bytearray, usar cast, mmap, struct e sockets com

    Ler mais

    Tempo de leitura: 5 minutos
    28/08/2026
    A person typing on a laptop with a Python programming book visible, capturing technology and learning.
    Python Avançado
    Foto de perfil de Leandro Hirt da Academify

    tracemalloc no Python: rastreie memória

    Aprenda tracemalloc no Python para medir picos, criar e comparar snapshots, filtrar alocações e diagnosticar crescimento de memória.

    Ler mais

    Tempo de leitura: 6 minutos
    28/08/2026
    Detailed shot of a Jungle Carpet Python (Morelia spilota cheynei) in its natural habitat.
    Python Avançado
    Foto de perfil de Leandro Hirt da Academify

    queue no Python: comunique threads

    Aprenda queue no Python para comunicar threads com FIFO, LIFO, prioridade, backpressure, task_done, join, sentinelas e shutdown seguro.

    Ler mais

    Tempo de leitura: 6 minutos
    28/08/2026
    Flat lay of a complete toolset neatly organized in a workshop setting, essential for auto repair tasks.
    Python Avançado
    Foto de perfil de Leandro Hirt da Academify

    selectors no Python: vários sockets

    Aprenda selectors no Python para multiplexar sockets, controlar leitura e escrita parcial, buffers, timeouts, wakeup e backpressure.

    Ler mais

    Tempo de leitura: 6 minutos
    28/08/2026
    Close-up view of a computer screen displaying code in a software development environment.
    Python Avançado
    Foto de perfil de Leandro Hirt da Academify

    contextvars no Python: contexto assíncrono

    Aprenda contextvars no Python para contexto por task, request IDs, logging, copy_context, propagação a threads e restauração segura com tokens.

    Ler mais

    Tempo de leitura: 6 minutos
    28/08/2026
    Person writing appointments on a calendar with a blue pen. High angle view.
    Python Avançado
    Foto de perfil de Leandro Hirt da Academify

    sched no Python: agende eventos

    Aprenda sched no Python para agendar eventos, usar prioridades, cancelar tarefas, criar recorrência sem drift e integrar executores.

    Ler mais

    Tempo de leitura: 6 minutos
    28/08/2026