Python Protocols: Tipagem Estrutural Sem Herança — 2026 | python.dev.br
Aprenda a usar Protocol em Python para tipagem estrutural. Duck typing seguro com mypy, exemplos práticos e quando usar Protocol vs ABC.
Python sempre foi uma linguagem de duck typing: se um objeto tem os métodos certos, ele funciona — independente da classe ou herança. Mas quando você adiciona type hints ao código, surge um problema: como tipar algo que depende de comportamento, e não de uma classe específica?
A resposta veio com a PEP 544 no Python 3.8: Protocol — uma forma de definir interfaces baseadas em estrutura, não em herança. Neste artigo, vamos explorar como Protocols funcionam, quando usá-los, e como eles transformam a qualidade do código Python.
O Problema: Duck Typing vs Type Hints
Imagine que você tem funções que trabalham com objetos “salváveis” — qualquer coisa que tenha um método .save():
class BancoDeDados:
def save(self, dados: dict) -> None:
print(f"Salvando no banco: {dados}")
class ArquivoJSON:
def save(self, dados: dict) -> None:
print(f"Salvando em JSON: {dados}")
class CacheRedis:
def save(self, dados: dict) -> None:
print(f"Salvando no Redis: {dados}")
def persistir(storage, dados: dict) -> None:
storage.save(dados)
Esse código funciona perfeitamente em tempo de execução — duck typing faz sua mágica. Mas o parâmetro storage não tem tipo definido. Se você colocar Any, perde a checagem estática. Se criar uma classe base abstrata, força todas as classes a herdarem dela.
É aqui que Protocol entra.
O Que É Protocol
Protocol é uma classe especial do módulo typing que define uma interface baseada em estrutura (structural subtyping), não em herança (nominal subtyping). Qualquer classe que tenha os métodos e atributos definidos no Protocol é considerada compatível — sem precisar herdar nada.
from typing import Protocol
class Salvavel(Protocol):
def save(self, dados: dict) -> None: ...
def persistir(storage: Salvavel, dados: dict) -> None:
storage.save(dados)
Agora o mypy sabe exatamente o que storage precisa ter. E nenhuma das classes (BancoDeDados, ArquivoJSON, CacheRedis) precisa ser modificada — elas já satisfazem o Protocol automaticamente:
# Tudo isso funciona sem herança!
persistir(BancoDeDados(), {"usuario": "Ana"}) # OK
persistir(ArquivoJSON(), {"usuario": "Bruno"}) # OK
persistir(CacheRedis(), {"usuario": "Carla"}) # OK
persistir("uma string", {"usuario": "Diego"}) # ERRO no mypy!
Protocol com Múltiplos Métodos e Atributos
Protocols podem definir métodos, propriedades e atributos de classe:
from typing import Protocol
class Repositorio(Protocol):
nome: str
def buscar(self, id: int) -> dict: ...
def salvar(self, dados: dict) -> int: ...
def deletar(self, id: int) -> bool: ...
class RepositorioPostgreSQL:
def __init__(self, connection_string: str):
self.nome = "PostgreSQL"
self.conn = connection_string
def buscar(self, id: int) -> dict:
# Implementação real com psycopg2
return {"id": id, "nome": "Exemplo"}
def salvar(self, dados: dict) -> int:
print(f"INSERT INTO tabela: {dados}")
return 1
def deletar(self, id: int) -> bool:
print(f"DELETE WHERE id = {id}")
return True
class RepositorioMemoria:
def __init__(self):
self.nome = "Memória"
self._dados: dict[int, dict] = {}
self._counter = 0
def buscar(self, id: int) -> dict:
return self._dados.get(id, {})
def salvar(self, dados: dict) -> int:
self._counter += 1
self._dados[self._counter] = dados
return self._counter
def deletar(self, id: int) -> bool:
return self._dados.pop(id, None) is not None
def processar_dados(repo: Repositorio) -> None:
print(f"Usando repositório: {repo.nome}")
id_novo = repo.salvar({"nome": "Python", "tipo": "linguagem"})
dados = repo.buscar(id_novo)
print(f"Recuperado: {dados}")
Ambas as classes satisfazem o Protocol Repositorio sem herdar dele. Isso é o poder da tipagem estrutural.
runtime_checkable: Verificação em Tempo de Execução
Por padrão, Protocols só são verificados por ferramentas estáticas como mypy. Para usar isinstance() com Protocols, use o decorador @runtime_checkable:
from typing import Protocol, runtime_checkable
@runtime_checkable
class Iteravel(Protocol):
def __iter__(self): ...
def __next__(self): ...
# Funciona com isinstance!
print(isinstance(iter([1, 2, 3]), Iteravel)) # True
print(isinstance(42, Iteravel)) # False
Atenção: runtime_checkable verifica apenas a existência dos métodos, não as assinaturas. Para checagem completa de tipos, use mypy.
Protocol vs ABC: Quando Usar Cada Um
Python tem duas formas de definir interfaces: Protocol (PEP 544) e ABC (Abstract Base Class). A escolha depende do cenário:
from abc import ABC, abstractmethod
from typing import Protocol
# ABC — herança explícita obrigatória
class StorageABC(ABC):
@abstractmethod
def save(self, dados: dict) -> None: ...
class MeuStorage(StorageABC): # PRECISA herdar
def save(self, dados: dict) -> None:
print(dados)
# Protocol — sem herança
class StorageProtocol(Protocol):
def save(self, dados: dict) -> None: ...
class OutroStorage: # NÃO precisa herdar
def save(self, dados: dict) -> None:
print(dados)
| Característica | Protocol | ABC |
|---|---|---|
| Herança necessária | Não | Sim |
| Verificação estática | mypy | mypy |
| isinstance() | Com @runtime_checkable | Sim, nativo |
| Código de terceiros | Funciona sem modificar | Precisa herdar |
| Erro em tempo de execução | Não (apenas estático) | Sim, ao instanciar |
| Melhor para | Interfaces implícitas, duck typing | Contratos explícitos |
Use Protocol quando:
- Quiser tipar código que segue duck typing
- Precisar de interfaces para classes que você não controla (bibliotecas de terceiros)
- Quiser manter o código desacoplado sem forçar herança
Use ABC quando:
- Quiser forçar um contrato explícito entre classes
- Precisar de erro em tempo de execução se um método não for implementado
- Tiver uma hierarquia de classes clara
Protocols Genéricos
Protocols podem ser combinados com generics para criar interfaces tipadas:
from typing import Protocol, TypeVar
T = TypeVar("T")
class Serializavel(Protocol[T]):
def serializar(self) -> T: ...
def deserializar(self, dados: T) -> None: ...
class Usuario:
def __init__(self, nome: str, idade: int):
self.nome = nome
self.idade = idade
def serializar(self) -> dict:
return {"nome": self.nome, "idade": self.idade}
def deserializar(self, dados: dict) -> None:
self.nome = dados["nome"]
self.idade = dados["idade"]
def salvar_e_restaurar(obj: Serializavel[dict]) -> dict:
dados = obj.serializar()
print(f"Serializado: {dados}")
return dados
# Funciona!
usuario = Usuario("Ana", 28)
salvar_e_restaurar(usuario)
Padrão Repository com Protocol
Um uso prático muito comum de Protocols é o padrão Repository, especialmente em projetos com FastAPI ou Django:
from typing import Protocol, TypeVar, Generic
from dataclasses import dataclass
T = TypeVar("T")
class Repository(Protocol[T]):
def get(self, id: int) -> T | None: ...
def list_all(self) -> list[T]: ...
def create(self, item: T) -> T: ...
def delete(self, id: int) -> bool: ...
@dataclass
class Produto:
id: int
nome: str
preco: float
class ProdutoRepositorySQL:
"""Implementação real com banco de dados"""
def get(self, id: int) -> Produto | None:
# SELECT * FROM produtos WHERE id = ?
return Produto(id=id, nome="Widget", preco=29.90)
def list_all(self) -> list[Produto]:
return [Produto(1, "Widget", 29.90), Produto(2, "Gadget", 49.90)]
def create(self, item: Produto) -> Produto:
print(f"INSERT: {item}")
return item
def delete(self, id: int) -> bool:
return True
class ProdutoRepositoryFake:
"""Implementação fake para testes"""
def __init__(self):
self._dados: dict[int, Produto] = {}
def get(self, id: int) -> Produto | None:
return self._dados.get(id)
def list_all(self) -> list[Produto]:
return list(self._dados.values())
def create(self, item: Produto) -> Produto:
self._dados[item.id] = item
return item
def delete(self, id: int) -> bool:
return self._dados.pop(id, None) is not None
def calcular_inventario(repo: Repository[Produto]) -> float:
"""Funciona com qualquer implementação do Repository"""
produtos = repo.list_all()
return sum(p.preco for p in produtos)
# Produção
total = calcular_inventario(ProdutoRepositorySQL())
# Testes
repo_fake = ProdutoRepositoryFake()
repo_fake.create(Produto(1, "Teste", 10.0))
total_teste = calcular_inventario(repo_fake)
Esse padrão facilita testes unitários porque você pode trocar a implementação real por um fake sem alterar o código de negócio.
Protocols na Biblioteca Padrão
O Python já usa Protocols internamente em vários lugares. O módulo typing define Protocols prontos para uso:
from typing import Sized, Hashable, Iterable, Iterator, Callable
# Sized — tem __len__
def processar(colecao: Sized) -> int:
return len(colecao)
# Iterable — tem __iter__
def somar_tudo(items: Iterable[float]) -> float:
return sum(items)
# Callable — pode ser chamado
def executar(func: Callable[[int, int], int], a: int, b: int) -> int:
return func(a, b)
# SupportsFloat, SupportsInt, SupportsBytes...
from typing import SupportsFloat
def converter(valor: SupportsFloat) -> float:
return float(valor)
Esses Protocols existem justamente para tipar duck typing de forma segura.
Combinando Protocol com Dataclasses
Dataclasses funcionam perfeitamente com Protocols:
from typing import Protocol
from dataclasses import dataclass
class Renderizavel(Protocol):
def to_html(self) -> str: ...
def to_text(self) -> str: ...
@dataclass
class Artigo:
titulo: str
conteudo: str
def to_html(self) -> str:
return f"<h1>{self.titulo}</h1><p>{self.conteudo}</p>"
def to_text(self) -> str:
return f"{self.titulo}\n\n{self.conteudo}"
@dataclass
class Comentario:
autor: str
texto: str
def to_html(self) -> str:
return f"<blockquote>{self.texto} — {self.autor}</blockquote>"
def to_text(self) -> str:
return f'"{self.texto}" — {self.autor}'
def exportar(items: list[Renderizavel], formato: str = "html") -> str:
if formato == "html":
return "\n".join(item.to_html() for item in items)
return "\n".join(item.to_text() for item in items)
Boas Práticas com Protocols
- Mantenha Protocols pequenos — prefira interfaces com 1-3 métodos. Protocols grandes são difíceis de satisfazer
- Nomeie pelo comportamento —
Salvavel,Renderizavel,Serializavelem vez deMeuProtocol - Use Protocol para dependências externas — quando tipando objetos de bibliotecas que você não controla
- Combine com decoradores para validações extras em tempo de execução
- Prefira Protocol a ABC para código novo que segue duck typing
- Documente o Protocol — mesmo sendo uma interface implícita, o docstring ajuda outros desenvolvedores
Conclusão
Protocols são a ponte entre o duck typing dinâmico do Python e a segurança da tipagem estática. Eles permitem que você escreva código tipado sem forçar herança, mantendo a flexibilidade que torna Python tão produtivo.
Se você já usa type hints e mypy, adotar Protocols é o próximo passo natural para código mais robusto e testável. E combinados com dataclasses e Pydantic, formam uma base sólida para projetos Python modernos.
Para quem se interessa por sistemas de tipos avançados, vale explorar o Rust, que tem um sistema de traits similar ao conceito de Protocols, mas verificado em tempo de compilação.
O Kotlin também implementa interfaces estruturais e é uma ótima opção para quem trabalha com JVM e quer tipagem segura sem boilerplate.
Equipe python.dev.br
Contribuidor do Python Brasil — Aprenda Python em Português