Python

Tipos Avançados em Python: Generic, Protocol, TypeVar e ParamSpec na Prática

19 min de leitura

Tipos Avançados em Python: Generic, Protocol, TypeVar e ParamSpec na Prática

Introdução aos Tipos Avançados em Python Python é uma linguagem dinamicamente tipada, mas desde a versão 3.5 ganhou suporte a type hints — anotações de tipo que melhoram a clareza do código e permitem verificação estática através de ferramentas como . Conforme seus projetos crescem em complexidade, você enfrentará situações onde tipos simples como , ou não são suficientes para expressar relações sofisticadas entre dados e funções. Os recursos que abordaremos neste artigo — , , e — são as ferramentas que transformam você de um programador que escreve type hints básicos para alguém capaz de expressar tipos complexos com precisão e elegância. Eles são utilizados extensivamente em bibliotecas modernas como FastAPI, SQLAlchemy, e Pydantic, e dominá-los é essencial para trabalhar com código Python profissional e escalável. TypeVar: Variáveis de Tipo e Polimorfismo Genérico O Problema que TypeVar Resolve Imagine que você precisa escrever uma função que funciona com qualquer tipo de sequência — listas, tuplas, strings — mas que

<h2>Introdução aos Tipos Avançados em Python</h2>

<p>Python é uma linguagem dinamicamente tipada, mas desde a versão 3.5 ganhou suporte a <strong>type hints</strong> — anotações de tipo que melhoram a clareza do código e permitem verificação estática através de ferramentas como <code>mypy</code>. Conforme seus projetos crescem em complexidade, você enfrentará situações onde tipos simples como <code>int</code>, <code>str</code> ou <code>list</code> não são suficientes para expressar relações sofisticadas entre dados e funções.</p>

<p>Os recursos que abordaremos neste artigo — <code>Generic</code>, <code>Protocol</code>, <code>TypeVar</code> e <code>ParamSpec</code> — são as ferramentas que transformam você de um programador que escreve type hints básicos para alguém capaz de expressar tipos complexos com precisão e elegância. Eles são utilizados extensivamente em bibliotecas modernas como FastAPI, SQLAlchemy, e Pydantic, e dominá-los é essencial para trabalhar com código Python profissional e escalável.</p>

<h2>TypeVar: Variáveis de Tipo e Polimorfismo Genérico</h2>

<h3>O Problema que TypeVar Resolve</h3>

<p>Imagine que você precisa escrever uma função que funciona com qualquer tipo de sequência — listas, tuplas, strings — mas que retorna sempre o mesmo tipo que recebeu como entrada. Sem <code>TypeVar</code>, você seria forçado a escrever múltiplas versões da mesma função ou aceitar tipos imprecisos.</p>

<p><code>TypeVar</code> permite criar uma <strong>variável de tipo</strong> que representa um tipo desconhecido no momento da escrita, mas que será vinculado a um tipo real quando a função for chamada. O compilador estático consegue rastrear essa relação e garantir que o tipo de saída corresponda ao tipo de entrada.</p>

<h3>Uso Básico de TypeVar</h3>

<pre><code class="language-python">from typing import TypeVar, List

T = TypeVar(&#039;T&#039;) # Cria uma variável de tipo não restrita

def primeira_elemento(sequencia: List[T]) -&gt; T:

&quot;&quot;&quot;Retorna o primeiro elemento de uma sequência, mantendo o tipo.&quot;&quot;&quot;

if not sequencia:

raise ValueError(&quot;Sequência vazia&quot;)

return sequencia[0]

Uso prático

numeros: List[int] = [1, 2, 3]

resultado_int = primeira_elemento(numeros) # mypy infere que resultado_int é int

nomes: List[str] = [&quot;Alice&quot;, &quot;Bob&quot;]

resultado_str = primeira_elemento(nomes) # mypy infere que resultado_str é str</code></pre>

<p>No exemplo acima, <code>T</code> é uma variável de tipo. Quando você chama <code>primeira_elemento(numeros)</code>, o tipo <code>T</code> é vinculado a <code>int</code>. Na chamada com <code>nomes</code>, <code>T</code> é vinculado a <code>str</code>. Isso é <strong>polimorfismo paramétrico</strong> — a mesma função funciona com múltiplos tipos mantendo segurança de tipo.</p>

<h3>TypeVar Restrito e Vinculado</h3>

<p>Nem sempre queremos aceitar qualquer tipo. Você pode restringir <code>TypeVar</code> a um conjunto específico de tipos usando <code>constraints</code>, ou vinculá-lo a um tipo pai com <code>bound</code>.</p>

<pre><code class="language-python">from typing import TypeVar, Union

TypeVar com restrições — T pode ser apenas int ou float

Numero = TypeVar(&#039;Numero&#039;, int, float)

def dobrar(valor: Numero) -&gt; Numero:

return valor * 2 # type: ignore

resultado1 = dobrar(5) # OK: int

resultado2 = dobrar(3.14) # OK: float

resultado3 = dobrar(&quot;x&quot;) # Erro em mypy: str não está em (int, float)

TypeVar com bound — T deve ser subtipo de uma classe

from abc import ABC

class Animal(ABC):

def fazer_som(self) -&gt; str:

pass

T_Animal = TypeVar(&#039;T_Animal&#039;, bound=Animal)

def fazer_som_animal(animal: T_Animal) -&gt; T_Animal:

print(animal.fazer_som())

return animal

class Cachorro(Animal):

def fazer_som(self) -&gt; str:

return &quot;Au au!&quot;

dog = Cachorro()

resultado = fazer_som_animal(dog) # OK, Cachorro é subtipo de Animal</code></pre>

<h2>Generic: Criando Classes Polimórficas Reutilizáveis</h2>

<h3>A Necessidade de Genéricos em Classes</h3>

<p>Quando você cria uma estrutura de dados como uma pilha, fila ou árvore, precisa trabalhar com elementos de qualquer tipo. <code>Generic</code> permite que você crie uma classe que seja parametrizada por tipo, mantendo segurança completa de tipos.</p>

<h3>Implementando um Generic Básico</h3>

<pre><code class="language-python">from typing import Generic, TypeVar, List, Optional

T = TypeVar(&#039;T&#039;)

class Pilha(Generic[T]):

&quot;&quot;&quot;Uma pilha genérica que funciona com qualquer tipo de elemento.&quot;&quot;&quot;

def __init__(self) -&gt; None:

self._elementos: List[T] = []

def empilhar(self, elemento: T) -&gt; None:

&quot;&quot;&quot;Adiciona um elemento no topo da pilha.&quot;&quot;&quot;

self._elementos.append(elemento)

def desempilhar(self) -&gt; T:

&quot;&quot;&quot;Remove e retorna o elemento do topo.&quot;&quot;&quot;

if not self._elementos:

raise IndexError(&quot;Pilha vazia&quot;)

return self._elementos.pop()

def vazia(self) -&gt; bool:

return len(self._elementos) == 0

def tamanho(self) -&gt; int:

return len(self._elementos)

Uso com tipos específicos

pilha_inteiros: Pilha[int] = Pilha()

pilha_inteiros.empilhar(10)

pilha_inteiros.empilhar(20)

valor = pilha_inteiros.desempilhar() # mypy sabe que valor é int

pilha_strings: Pilha[str] = Pilha()

pilha_strings.empilhar(&quot;Hello&quot;)

pilha_strings.empilhar(&quot;World&quot;)

palavra = pilha_strings.desempilhar() # mypy sabe que palavra é str</code></pre>

<p>Observe que <code>Pilha[int]</code> e <code>Pilha[str]</code> são tipos <strong>diferentes</strong> do ponto de vista do verificador estático. Isso previne erros como tentar desempilhar de uma pilha de inteiros e atribuir a uma variável de string — o mypy rejeitaria esse código.</p>

<h3>Genéricos com Múltiplos Parâmetros de Tipo</h3>

<p>Frequentemente você precisará de mais de um parâmetro de tipo. Um exemplo clássico é um dicionário genérico ou um par de valores:</p>

<pre><code class="language-python">from typing import Generic, TypeVar

K = TypeVar(&#039;K&#039;) # Chave

V = TypeVar(&#039;V&#039;) # Valor

class Par(Generic[K, V]):

&quot;&quot;&quot;Representa um par chave-valor genérico.&quot;&quot;&quot;

def __init__(self, chave: K, valor: V) -&gt; None:

self.chave = chave

self.valor = valor

def obter_chave(self) -&gt; K:

return self.chave

def obter_valor(self) -&gt; V:

return self.valor

Uso

par_nome_idade: Par[str, int] = Par(&quot;Alice&quot;, 30)

nome: str = par_nome_idade.obter_chave()

idade: int = par_nome_idade.obter_valor()

par_coordenadas: Par[float, float] = Par(10.5, 20.3)

x: float = par_coordenadas.obter_chave()

y: float = par_coordenadas.obter_valor()</code></pre>

<h2>Protocol: Tipagem Estrutural e Interfaces Implícitas</h2>

<h3>Diferença entre Herança Nominal e Tipagem Estrutural</h3>

<p>Python tradicionalmente usa <strong>tipagem nominal</strong> — você verifica se um objeto é instância de uma classe específica. Mas frequentemente o que realmente importa é o que um objeto <strong>consegue fazer</strong>, não sua linhagem de classes. Se algo tem um método <code>__len__()</code>, você pode tratá-lo como &quot;algo que tem comprimento&quot;, independentemente de herdar de uma classe específica.</p>

<p><code>Protocol</code> implementa <strong>tipagem estrutural</strong> — você define uma interface baseada no contrato de métodos que um tipo deve ter, sem precisar de herança explícita. Se um objeto tem os métodos certos com as assinaturas corretas, ele satisfaz o protocol.</p>

<h3>Definindo e Usando Protocols</h3>

<pre><code class="language-python">from typing import Protocol, runtime_checkable

@runtime_checkable

class Desenhavel(Protocol):

&quot;&quot;&quot;Protocol para qualquer coisa que possa ser desenhada.&quot;&quot;&quot;

def desenhar(self) -&gt; None:

&quot;&quot;&quot;Método que deve ser implementado.&quot;&quot;&quot;

...

@runtime_checkable

class Persistivel(Protocol):

&quot;&quot;&quot;Protocol para qualquer coisa que possa ser salva.&quot;&quot;&quot;

def salvar(self, caminho: str) -&gt; None:

&quot;&quot;&quot;Salva o objeto em um arquivo.&quot;&quot;&quot;

...

class Circulo:

&quot;&quot;&quot;Uma classe que implementa Desenhavel, sem herança explícita.&quot;&quot;&quot;

def __init__(self, raio: float) -&gt; None:

self.raio = raio

def desenhar(self) -&gt; None:

print(f&quot;Desenhando círculo com raio {self.raio}&quot;)

class Imagem:

&quot;&quot;&quot;Uma classe que implementa tanto Desenhavel quanto Persistivel.&quot;&quot;&quot;

def __init__(self, caminho: str) -&gt; None:

self.caminho = caminho

def desenhar(self) -&gt; None:

print(f&quot;Exibindo imagem: {self.caminho}&quot;)

def salvar(self, caminho: str) -&gt; None:

print(f&quot;Salvando em {caminho}&quot;)

def renderizar(obj: Desenhavel) -&gt; None:

&quot;&quot;&quot;Aceita qualquer coisa que tenha um método desenhar().&quot;&quot;&quot;

obj.desenhar()

def processar(obj: Persistivel) -&gt; None:

&quot;&quot;&quot;Aceita qualquer coisa que possa ser salva.&quot;&quot;&quot;

obj.salvar(&quot;/tmp/backup&quot;)

Uso

circulo = Circulo(5.0)

renderizar(circulo) # Funciona! Circulo tem desenhar()

imagem = Imagem(&quot;foto.png&quot;)

renderizar(imagem) # Funciona!

processar(imagem) # Funciona! Imagem tem salvar()

Verificação em tempo de execução (com @runtime_checkable)

print(isinstance(circulo, Desenhavel)) # True

print(isinstance(circulo, Persistivel)) # False</code></pre>

<h3>Protocols com Atributos</h3>

<p><code>Protocol</code> não é limitado a métodos — você também pode especificar atributos obrigatórios:</p>

<pre><code class="language-python">from typing import Protocol

class Identificavel(Protocol):

&quot;&quot;&quot;Protocol para entidades com identificador.&quot;&quot;&quot;

id: int

nome: str

def descrever(self) -&gt; str:

&quot;&quot;&quot;Retorna uma descrição textual.&quot;&quot;&quot;

...

class Usuario:

def __init__(self, id: int, nome: str) -&gt; None:

self.id = id

self.nome = nome

def descrever(self) -&gt; str:

return f&quot;Usuário {self.nome} (ID: {self.id})&quot;

class Produto:

def __init__(self, id: int, nome: str) -&gt; None:

self.id = id

self.nome = nome

def descrever(self) -&gt; str:

return f&quot;Produto: {self.nome} (SKU: {self.id})&quot;

def exibir_info(obj: Identificavel) -&gt; None:

&quot;&quot;&quot;Funciona com qualquer coisa que tenha id, nome e descrever().&quot;&quot;&quot;

print(f&quot;ID: {obj.id}, Nome: {obj.nome}&quot;)

print(obj.descrever())

usuario = Usuario(1, &quot;Alice&quot;)

produto = Produto(101, &quot;Notebook&quot;)

exibir_info(usuario) # Funciona!

exibir_info(produto) # Funciona!</code></pre>

<p>A vantagem aqui é clara: você não precisa que <code>Usuario</code> e <code>Produto</code> herdem de uma classe base comum. Se eles satisfazem o contrato estrutural, eles são aceitos.</p>

<h2>ParamSpec: Preservando Assinaturas de Função</h2>

<h3>O Problema: Decoradores que Perdem Tipo</h3>

<p>Quando você cria um decorador, frequentemente quer que ele mantenha a assinatura exata da função decorada — os parâmetros, tipos de retorno, etc. Sem <code>ParamSpec</code>, isso é praticamente impossível de expressar de forma precisa com type hints.</p>

<pre><code class="language-python"># Exemplo SEM ParamSpec — assinatura imprecisa

from typing import Callable, TypeVar, Any

T = TypeVar(&#039;T&#039;)

def meu_decorador(func: Callable[..., Any]) -&gt; Callable[..., Any]:

&quot;&quot;&quot;Decorador que registra chamadas, mas perde informação de tipo.&quot;&quot;&quot;

def wrapper(args: Any, *kwargs: Any) -&gt; Any:

print(f&quot;Chamando {func.__name__}&quot;)

return func(args, *kwargs)

return wrapper

@meu_decorador

def saudacao(nome: str, idade: int) -&gt; str:

return f&quot;Olá {nome}, você tem {idade} anos&quot;

mypy perde completamente a assinatura original!

resultado = saudacao(&quot;Bob&quot;, 25) # mypy não sabe que resultado é str</code></pre>

<h3>Usando ParamSpec para Preservar a Assinatura</h3>

<pre><code class="language-python">from typing import ParamSpec, Callable, TypeVar

P = ParamSpec(&#039;P&#039;)

R = TypeVar(&#039;R&#039;)

def meu_decorador(func: Callable[P, R]) -&gt; Callable[P, R]:

&quot;&quot;&quot;Decorador que preserva a assinatura e tipo de retorno exatos.&quot;&quot;&quot;

def wrapper(args: P.args, *kwargs: P.kwargs) -&gt; R:

print(f&quot;Chamando {func.__name__}&quot;)

return func(args, *kwargs)

return wrapper

@meu_decorador

def saudacao(nome: str, idade: int) -&gt; str:

return f&quot;Olá {nome}, você tem {idade} anos&quot;

@meu_decorador

def calcular(a: float, b: float) -&gt; float:

return a + b

Agora mypy preserva as assinaturas!

resultado1: str = saudacao(&quot;Bob&quot;, 25) # OK: resultado1 é str

resultado2: float = calcular(3.14, 2.86) # OK: resultado2 é float

Estes causariam erro em mypy:

resultado3: int = saudacao(&quot;Bob&quot;, 25) # Erro: esperava int, não str

resultado4 = saudacao(&quot;Bob&quot;, &quot;vinte&quot;) # Erro: idade deve ser int</code></pre>

<h3>Caso Real: Decorador com Logging e Timing</h3>

<pre><code class="language-python">from typing import ParamSpec, Callable, TypeVar

import time

P = ParamSpec(&#039;P&#039;)

R = TypeVar(&#039;R&#039;)

def timer_e_log(func: Callable[P, R]) -&gt; Callable[P, R]:

&quot;&quot;&quot;Decorador que mede tempo de execução e registra entrada/saída.&quot;&quot;&quot;

def wrapper(args: P.args, *kwargs: P.kwargs) -&gt; R:

inicio = time.time()

print(f&quot;&gt;&gt;&gt; {func.__name__} chamada com args={args}, kwargs={kwargs}&quot;)

resultado = func(args, *kwargs)

duracao = time.time() - inicio

print(f&quot;&lt;&lt;&lt; {func.__name__} retornou {resultado} em {duracao:.4f}s&quot;)

return resultado

return wrapper

@timer_e_log

def buscar_usuario(id: int) -&gt; dict:

time.sleep(0.1)

return {&quot;id&quot;: id, &quot;nome&quot;: &quot;Alice&quot;}

@timer_e_log

def concatenar(a: str, b: str, separador: str = &quot; &quot;) -&gt; str:

return a + separador + b

Uso

usuario: dict = buscar_usuario(1)

mensagem: str = concatenar(&quot;Olá&quot;, &quot;Mundo&quot;, separador=&quot;, &quot;)</code></pre>

<p>A saída será:</p>

<pre><code>&gt;&gt;&gt; buscar_usuario chamada com args=(1,), kwargs={}

&lt;&lt;&lt; buscar_usuario retornou {&#039;id&#039;: 1, &#039;nome&#039;: &#039;Alice&#039;} em 0.1001s

&gt;&gt;&gt; concatenar chamada com args=(&#039;Olá&#039;, &#039;Mundo&#039;), kwargs={&#039;separador&#039;: &#039;, &#039;}

&lt;&lt;&lt; concatenar retornou Olá, Mundo em 0.0001s</code></pre>

<h2>Combinando Generic, Protocol, TypeVar e ParamSpec</h2>

<p>Para consolidar o aprendizado, vamos criar um exemplo real que combina todos esses conceitos: um sistema de cache genérico com decorador type-safe.</p>

<pre><code class="language-python">from typing import Protocol, TypeVar, Generic, ParamSpec, Callable, Dict, Any

import functools

Protocol para coisas que podem ser hashable (chaves de cache)

K = TypeVar(&#039;K&#039;)

V = TypeVar(&#039;V&#039;)

P = ParamSpec(&#039;P&#039;)

R = TypeVar(&#039;R&#039;)

class Armazenavel(Protocol):

&quot;&quot;&quot;Protocol para valores que podem ser armazenados em cache.&quot;&quot;&quot;

def para_cache(self) -&gt; str:

...

class Cache(Generic[K, V]):

&quot;&quot;&quot;Cache genérico type-safe.&quot;&quot;&quot;

def __init__(self) -&gt; None:

self._dados: Dict[K, V] = {}

def obter(self, chave: K) -&gt; V | None:

return self._dados.get(chave)

def armazenar(self, chave: K, valor: V) -&gt; None:

self._dados[chave] = valor

def limpar(self) -&gt; None:

self._dados.clear()

def memoize_com_tipo(cache: Cache[str, R]) -&gt; Callable[[Callable[P, R]], Callable[P, R]]:

&quot;&quot;&quot;Decorador que usa cache genérico tipado para memoização.&quot;&quot;&quot;

def decorador(func: Callable[P, R]) -&gt; Callable[P, R]:

@functools.wraps(func)

def wrapper(args: P.args, *kwargs: P.kwargs) -&gt; R:

chave = f&quot;{func.__name__}:{str(args)}:{str(kwargs)}&quot;

resultado_em_cache = cache.obter(chave)

if resultado_em_cache is not None:

print(f&quot;[CACHE HIT] {chave}&quot;)

return resultado_em_cache

print(f&quot;[CACHE MISS] Calculando {chave}&quot;)

resultado = func(args, *kwargs)

cache.armazenar(chave, resultado)

return resultado

return wrapper

return decorador

Uso

cache_numeros: Cache[str, int] = Cache()

@memoize_com_tipo(cache_numeros)

def fibonacci(n: int) -&gt; int:

if n &lt;= 1:

return n

return fibonacci(n - 1) + fibonacci(n - 2)

print(fibonacci(5)) # Calcula

print(fibonacci(5)) # Do cache

print(fibonacci(6)) # Calcula

Type-safe: mypy sabe que fibonacci retorna int

resultado: int = fibonacci(10)</code></pre>

<h2>Conclusão</h2>

<p>Aprendemos três conceitos fundamentais que elevam a qualidade da tipagem em Python: <strong>TypeVar</strong> permite polimorfismo seguro e reutilização de código genérico através de variáveis de tipo; <strong>Generic</strong> permite criar classes parametrizadas por tipo, garantindo que estruturas de dados funcionem com qualquer tipo mantendo segurança completa; <strong>Protocol</strong> implementa tipagem estrutural, permitindo criar interfaces implícitas baseadas no que um objeto consegue fazer, não em sua linhagem de classes; e <strong>ParamSpec</strong> preserva assinaturas de função em decoradores, permitindo ferramentas estáticas rastrear exatamente quais parâmetros e retornos são aceitos.</p>

<p>Quando usadas juntas, essas ferramentas transformam seu código Python em algo tão seguro quanto linguagens compiladas estaticamente, mas mantendo a flexibilidade e expressividade de Python. A chave é entender que type hints não são apenas para documentação — eles são contratos que permitem que editores, linters e o seu próprio cérebro entendam exatamente o que cada função faz e o que cada classe contém.</p>

<h2>Referências</h2>

<ul>

<li><a href="https://docs.python.org/3/library/typing.html" target="_blank" rel="noopener noreferrer">Python typing — Official Documentation</a></li>

<li><a href="https://www.python.org/dev/peps/pep-0484/" target="_blank" rel="noopener noreferrer">PEP 484 — Type Hints</a></li>

<li><a href="https://www.python.org/dev/peps/pep-0544/" target="_blank" rel="noopener noreferrer">PEP 544 — Protocols: Structural subtyping</a></li>

<li><a href="https://www.oreilly.com/library/view/fluent-python-2nd/9781492126249/" target="_blank" rel="noopener noreferrer">Fluent Python (2nd Edition) — Luciano Ramalho</a></li>

<li><a href="https://mypy.readthedocs.io/" target="_blank" rel="noopener noreferrer">mypy Documentation — Type Checking</a></li>

</ul>

<p>&lt;!-- FIM --&gt;</p>

Comentários

Mais em Python

O que Todo Dev Deve Saber sobre Alembic em Python: Migrations Versionadas com SQLAlchemy
O que Todo Dev Deve Saber sobre Alembic em Python: Migrations Versionadas com SQLAlchemy

O que é Alembic e Por Que Você Precisa Dele Alembic é uma ferramenta de versi...

Dominando Manipulação de Arquivos em Python: CSV, JSON, XML e Excel com openpyxl em Projetos Reais
Dominando Manipulação de Arquivos em Python: CSV, JSON, XML e Excel com openpyxl em Projetos Reais

Introdução: Por que dominar manipulação de arquivos? A manipulação de arquivo...

Boas Práticas de Generators e yield em Python: Lazy Evaluation e Pipelines de Dados para Times Ágeis
Boas Práticas de Generators e yield em Python: Lazy Evaluation e Pipelines de Dados para Times Ágeis

Entendendo Generators: O Que Realmente São Um generator em Python é uma funçã...