---
title: "Python Protocols: Tipagem Estrutural Sem Herança — 2026 | python.dev.br"
url: "https://python.dev.br/blog/python-protocols-tipagem-estrutural/"
markdown_url: "https://python.dev.br/blog/python-protocols-tipagem-estrutural.MD"
description: "Aprenda a usar Protocol em Python para tipagem estrutural. Duck typing seguro com mypy, exemplos práticos e quando usar Protocol vs ABC."
date: "2026-03-29"
author: "Equipe python.dev.br"
---

# 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](/blog/tipagem-estatica-python-mypy/) 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()`:

```python
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.

```python
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](/blog/tipagem-estatica-python-mypy/) sabe exatamente o que `storage` precisa ter. E nenhuma das classes (`BancoDeDados`, `ArquivoJSON`, `CacheRedis`) precisa ser modificada — elas já satisfazem o Protocol automaticamente:

```python
# 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:

```python
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`:

```python
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:

```python
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:

```python
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](/blog/design-patterns-python/), especialmente em projetos com [FastAPI](/blog/apis-rest-com-fastapi/) ou Django:

```python
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](/blog/testes-unitarios-python/) 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:

```python
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](/blog/dataclasses-python-guia-completo/) funcionam perfeitamente com Protocols:

```python
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 comportamento** — `Salvavel`, `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](/blog/decoradores-python-guia-pratico/)** 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](/blog/tipagem-estatica-python-mypy/) e [mypy](/glossario/type-hints/), adotar Protocols é o próximo passo natural para código mais robusto e testável. E combinados com [dataclasses](/blog/dataclasses-python-guia-completo/) e [Pydantic](/blog/pydantic-validacao-dados-python/), formam uma base sólida para projetos Python modernos.

> Para quem se interessa por sistemas de tipos avançados, vale explorar o <a href="https://rustlang.com.br/" target="_blank" rel="noopener" onclick="umami.track('portfolio-site-click', { destination: 'rustlang.com.br' })">Rust</a>, que tem um sistema de traits similar ao conceito de Protocols, mas verificado em tempo de compilação.

> O <a href="https://kotlin.dev.br/" target="_blank" rel="noopener" onclick="umami.track('portfolio-site-click', { destination: 'kotlin.dev.br' })">Kotlin</a> também implementa interfaces estruturais e é uma ótima opção para quem trabalha com JVM e quer tipagem segura sem boilerplate.
