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.

8 min de leitura Equipe python.dev.br

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ísticaProtocolABC
Herança necessáriaNãoSim
Verificação estáticamypymypy
isinstance()Com @runtime_checkableSim, nativo
Código de terceirosFunciona sem modificarPrecisa herdar
Erro em tempo de execuçãoNão (apenas estático)Sim, ao instanciar
Melhor paraInterfaces implícitas, duck typingContratos 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

  1. Mantenha Protocols pequenos — prefira interfaces com 1-3 métodos. Protocols grandes são difíceis de satisfazer
  2. Nomeie pelo comportamentoSalvavel, Renderizavel, Serializavel em vez de MeuProtocol
  3. Use Protocol para dependências externas — quando tipando objetos de bibliotecas que você não controla
  4. Combine com decoradores para validações extras em tempo de execução
  5. Prefira Protocol a ABC para código novo que segue duck typing
  6. 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.

E

Equipe python.dev.br

Contribuidor do Python Brasil — Aprenda Python em Português