<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('T') # Cria uma variável de tipo não restrita
def primeira_elemento(sequencia: List[T]) -> T:
"""Retorna o primeiro elemento de uma sequência, mantendo o tipo."""
if not sequencia:
raise ValueError("Sequência vazia")
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] = ["Alice", "Bob"]
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('Numero', int, float)
def dobrar(valor: Numero) -> Numero:
return valor * 2 # type: ignore
resultado1 = dobrar(5) # OK: int
resultado2 = dobrar(3.14) # OK: float
resultado3 = dobrar("x") # 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) -> str:
pass
T_Animal = TypeVar('T_Animal', bound=Animal)
def fazer_som_animal(animal: T_Animal) -> T_Animal:
print(animal.fazer_som())
return animal
class Cachorro(Animal):
def fazer_som(self) -> str:
return "Au au!"
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('T')
class Pilha(Generic[T]):
"""Uma pilha genérica que funciona com qualquer tipo de elemento."""
def __init__(self) -> None:
self._elementos: List[T] = []
def empilhar(self, elemento: T) -> None:
"""Adiciona um elemento no topo da pilha."""
self._elementos.append(elemento)
def desempilhar(self) -> T:
"""Remove e retorna o elemento do topo."""
if not self._elementos:
raise IndexError("Pilha vazia")
return self._elementos.pop()
def vazia(self) -> bool:
return len(self._elementos) == 0
def tamanho(self) -> 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("Hello")
pilha_strings.empilhar("World")
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('K') # Chave
V = TypeVar('V') # Valor
class Par(Generic[K, V]):
"""Representa um par chave-valor genérico."""
def __init__(self, chave: K, valor: V) -> None:
self.chave = chave
self.valor = valor
def obter_chave(self) -> K:
return self.chave
def obter_valor(self) -> V:
return self.valor
Uso
par_nome_idade: Par[str, int] = Par("Alice", 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 "algo que tem comprimento", 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):
"""Protocol para qualquer coisa que possa ser desenhada."""
def desenhar(self) -> None:
"""Método que deve ser implementado."""
...
@runtime_checkable
class Persistivel(Protocol):
"""Protocol para qualquer coisa que possa ser salva."""
def salvar(self, caminho: str) -> None:
"""Salva o objeto em um arquivo."""
...
class Circulo:
"""Uma classe que implementa Desenhavel, sem herança explícita."""
def __init__(self, raio: float) -> None:
self.raio = raio
def desenhar(self) -> None:
print(f"Desenhando círculo com raio {self.raio}")
class Imagem:
"""Uma classe que implementa tanto Desenhavel quanto Persistivel."""
def __init__(self, caminho: str) -> None:
self.caminho = caminho
def desenhar(self) -> None:
print(f"Exibindo imagem: {self.caminho}")
def salvar(self, caminho: str) -> None:
print(f"Salvando em {caminho}")
def renderizar(obj: Desenhavel) -> None:
"""Aceita qualquer coisa que tenha um método desenhar()."""
obj.desenhar()
def processar(obj: Persistivel) -> None:
"""Aceita qualquer coisa que possa ser salva."""
obj.salvar("/tmp/backup")
Uso
circulo = Circulo(5.0)
renderizar(circulo) # Funciona! Circulo tem desenhar()
imagem = Imagem("foto.png")
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):
"""Protocol para entidades com identificador."""
id: int
nome: str
def descrever(self) -> str:
"""Retorna uma descrição textual."""
...
class Usuario:
def __init__(self, id: int, nome: str) -> None:
self.id = id
self.nome = nome
def descrever(self) -> str:
return f"Usuário {self.nome} (ID: {self.id})"
class Produto:
def __init__(self, id: int, nome: str) -> None:
self.id = id
self.nome = nome
def descrever(self) -> str:
return f"Produto: {self.nome} (SKU: {self.id})"
def exibir_info(obj: Identificavel) -> None:
"""Funciona com qualquer coisa que tenha id, nome e descrever()."""
print(f"ID: {obj.id}, Nome: {obj.nome}")
print(obj.descrever())
usuario = Usuario(1, "Alice")
produto = Produto(101, "Notebook")
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('T')
def meu_decorador(func: Callable[..., Any]) -> Callable[..., Any]:
"""Decorador que registra chamadas, mas perde informação de tipo."""
def wrapper(args: Any, *kwargs: Any) -> Any:
print(f"Chamando {func.__name__}")
return func(args, *kwargs)
return wrapper
@meu_decorador
def saudacao(nome: str, idade: int) -> str:
return f"Olá {nome}, você tem {idade} anos"
mypy perde completamente a assinatura original!
resultado = saudacao("Bob", 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('P')
R = TypeVar('R')
def meu_decorador(func: Callable[P, R]) -> Callable[P, R]:
"""Decorador que preserva a assinatura e tipo de retorno exatos."""
def wrapper(args: P.args, *kwargs: P.kwargs) -> R:
print(f"Chamando {func.__name__}")
return func(args, *kwargs)
return wrapper
@meu_decorador
def saudacao(nome: str, idade: int) -> str:
return f"Olá {nome}, você tem {idade} anos"
@meu_decorador
def calcular(a: float, b: float) -> float:
return a + b
Agora mypy preserva as assinaturas!
resultado1: str = saudacao("Bob", 25) # OK: resultado1 é str
resultado2: float = calcular(3.14, 2.86) # OK: resultado2 é float
Estes causariam erro em mypy:
resultado3: int = saudacao("Bob", 25) # Erro: esperava int, não str
resultado4 = saudacao("Bob", "vinte") # 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('P')
R = TypeVar('R')
def timer_e_log(func: Callable[P, R]) -> Callable[P, R]:
"""Decorador que mede tempo de execução e registra entrada/saída."""
def wrapper(args: P.args, *kwargs: P.kwargs) -> R:
inicio = time.time()
print(f">>> {func.__name__} chamada com args={args}, kwargs={kwargs}")
resultado = func(args, *kwargs)
duracao = time.time() - inicio
print(f"<<< {func.__name__} retornou {resultado} em {duracao:.4f}s")
return resultado
return wrapper
@timer_e_log
def buscar_usuario(id: int) -> dict:
time.sleep(0.1)
return {"id": id, "nome": "Alice"}
@timer_e_log
def concatenar(a: str, b: str, separador: str = " ") -> str:
return a + separador + b
Uso
usuario: dict = buscar_usuario(1)
mensagem: str = concatenar("Olá", "Mundo", separador=", ")</code></pre>
<p>A saída será:</p>
<pre><code>>>> buscar_usuario chamada com args=(1,), kwargs={}
<<< buscar_usuario retornou {'id': 1, 'nome': 'Alice'} em 0.1001s
>>> concatenar chamada com args=('Olá', 'Mundo'), kwargs={'separador': ', '}
<<< 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('K')
V = TypeVar('V')
P = ParamSpec('P')
R = TypeVar('R')
class Armazenavel(Protocol):
"""Protocol para valores que podem ser armazenados em cache."""
def para_cache(self) -> str:
...
class Cache(Generic[K, V]):
"""Cache genérico type-safe."""
def __init__(self) -> None:
self._dados: Dict[K, V] = {}
def obter(self, chave: K) -> V | None:
return self._dados.get(chave)
def armazenar(self, chave: K, valor: V) -> None:
self._dados[chave] = valor
def limpar(self) -> None:
self._dados.clear()
def memoize_com_tipo(cache: Cache[str, R]) -> Callable[[Callable[P, R]], Callable[P, R]]:
"""Decorador que usa cache genérico tipado para memoização."""
def decorador(func: Callable[P, R]) -> Callable[P, R]:
@functools.wraps(func)
def wrapper(args: P.args, *kwargs: P.kwargs) -> R:
chave = f"{func.__name__}:{str(args)}:{str(kwargs)}"
resultado_em_cache = cache.obter(chave)
if resultado_em_cache is not None:
print(f"[CACHE HIT] {chave}")
return resultado_em_cache
print(f"[CACHE MISS] Calculando {chave}")
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) -> int:
if n <= 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><!-- FIM --></p>