Python Admin

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

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

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 mypy. Conforme seus projetos crescem em complexidade, você enfrentará situações onde tipos simples como int, str ou list não são suficientes para expressar relações sofisticadas entre dados e funções.

Os recursos que abordaremos neste artigo — Generic, Protocol, TypeVar e ParamSpec — 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 retorna sempre o mesmo tipo que recebeu como entrada. Sem TypeVar, você seria forçado a escrever múltiplas versões da mesma função ou aceitar tipos imprecisos.

TypeVar permite criar uma variável de tipo 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.

Uso Básico de TypeVar

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

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

TypeVar Restrito e Vinculado

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

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

Generic: Criando Classes Polimórficas Reutilizáveis

A Necessidade de Genéricos em Classes

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

Implementando um Generic Básico

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

Observe que Pilha[int] e Pilha[str] são tipos diferentes 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.

Genéricos com Múltiplos Parâmetros de Tipo

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:

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()

Protocol: Tipagem Estrutural e Interfaces Implícitas

Diferença entre Herança Nominal e Tipagem Estrutural

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

Protocol implementa tipagem estrutural — 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.

Definindo e Usando Protocols

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

Protocols com Atributos

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

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!

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

ParamSpec: Preservando Assinaturas de Função

O Problema: Decoradores que Perdem Tipo

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 ParamSpec, isso é praticamente impossível de expressar de forma precisa com type hints.

# 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

Usando ParamSpec para Preservar a Assinatura

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

Caso Real: Decorador com Logging e Timing

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=", ")

A saída será:

>>> 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

Combinando Generic, Protocol, TypeVar e ParamSpec

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

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)

Conclusão

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

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.

Referências


Artigos relacionados