Tipagem avançada: Union, Optional, Generic
1. Introdução à tipagem avançada em Python
Python, como linguagem dinamicamente tipada, sempre priorizou flexibilidade. No entanto, com o crescimento de projetos em escala empresarial, a necessidade de ferramentas que auxiliem na detecção precoce de erros tornou-se evidente. Foi aí que os type hints (PEP 484) revolucionaram a forma como escrevemos Python, permitindo que ferramentas como mypy e pyright analisem estaticamente o código.
A tipagem básica (int, str, List[int]) resolve muitos problemas, mas cenários reais exigem mais: funções que aceitam múltiplos tipos, parâmetros opcionais e estruturas de dados genéricas reutilizáveis. O módulo typing oferece exatamente essas ferramentas.
É importante entender a diferença entre tipagem nominal (classes e herança) e estrutural (baseada na forma dos objetos). Python adota predominantemente a nominal, mas com Protocol (PEP 544) podemos usar tipagem estrutural quando necessário.
2. Union: múltiplos tipos possíveis
Union permite declarar que um valor pode ser de um entre vários tipos específicos. A sintaxe clássica é Union[int, str], mas desde Python 3.10 podemos usar a notação mais concisa int | str.
from typing import Union
# Sintaxe tradicional (Python 3.5+)
def processar_id(id: Union[int, str]) -> str:
return f"ID processado: {id}"
# Sintaxe moderna (Python 3.10+)
def processar_id_moderno(id: int | str) -> str:
return f"ID processado: {id}"
# Uso em retorno
def buscar_usuario(uid: int) -> dict | None:
# Simulando busca em banco
usuarios = {1: {"nome": "Alice"}, 2: {"nome": "Bob"}}
return usuarios.get(uid)
Cuidado com Union aninhadas: Union[Union[int, str], float] é automaticamente simplificado para Union[int, str, float]. Prefira sempre a forma mais plana.
3. Optional: tratando valores que podem ser None
Optional[X] é um açúcar sintático para Union[X, None]. Indica que um valor pode ser do tipo X ou None, sendo essencial para parâmetros opcionais e funções que podem falhar.
from typing import Optional
# Parâmetro opcional com Optional
def saudacao(nome: Optional[str] = None) -> str:
if nome is None:
return "Olá, visitante!"
return f"Olá, {nome}!"
# Retorno que pode ser None
def dividir(a: float, b: float) -> Optional[float]:
if b == 0:
return None
return a / b
# Boa prática: prefira Optional[X] a Union[X, None]
# para deixar explícita a intencionalidade do None
Boas práticas: Use Optional quando o None tem significado semântico (ausência de valor). Para valores padrão que são opcionais mas nunca serão None, use apenas o valor padrão sem tipagem especial.
4. Generic: criando tipos parametrizáveis
Generic permite criar classes, funções e tipos que funcionam com diferentes tipos de dados, mantendo a segurança de tipos. O TypeVar é a peça fundamental.
from typing import Generic, TypeVar, List
T = TypeVar('T')
# Classe genérica: Pilha
class Pilha(Generic[T]):
def __init__(self) -> None:
self._itens: List[T] = []
def push(self, item: T) -> None:
self._itens.append(item)
def pop(self) -> T:
return self._itens.pop()
def vazia(self) -> bool:
return len(self._itens) == 0
# Uso prático
pilha_int = Pilha[int]()
pilha_int.push(10)
pilha_int.push(20)
print(pilha_int.pop()) # 20
pilha_str = Pilha[str]()
pilha_str.push("Python")
pilha_str.push("Tipagem")
print(pilha_str.pop()) # "Tipagem"
Funções genéricas também são possíveis:
from typing import Sequence, TypeVar
T = TypeVar('T')
def primeiro_elemento(seq: Sequence[T]) -> T:
"""Retorna o primeiro elemento de uma sequência."""
return seq[0]
print(primeiro_elemento([1, 2, 3])) # 1
print(primeiro_elemento("Python")) # "P"
5. TypeVar: restrições e variância
TypeVar pode ser configurado com limites (bound) e restrições (constraints) para controlar quais tipos são aceitos.
from typing import TypeVar, Generic
# TypeVar com bound (limite superior)
class Animal:
def som(self) -> str:
return "..."
class Cachorro(Animal):
def som(self) -> str:
return "Au au"
T_Animal = TypeVar('T_Animal', bound=Animal)
class Abrigo(Generic[T_Animal]):
def __init__(self, animal: T_Animal):
self.animal = animal
def ouvir_som(self) -> str:
return self.animal.som()
# TypeVar com constraints (tipos específicos permitidos)
T_Num = TypeVar('T_Num', int, float)
def somar(a: T_Num, b: T_Num) -> T_Num:
return a + b # type: ignore
# Variância: covariant, contravariant, invariant
from typing import Iterator
# Iterator é covariante (produz T)
class MeuIterador(Generic[T]):
def __iter__(self) -> Iterator[T]:
...
Covariância (covariant=True) é usada quando o tipo é apenas produzido (como em Iterator). Contravariância (contravariant=True) quando o tipo é apenas consumido (como em Callable). Invariância (padrão) quando o tipo é tanto produzido quanto consumido.
6. Tipos especiais: Any, NoReturn, Literal
from typing import Any, NoReturn, Literal
# Any: desliga a verificação de tipos
def log_mensagem(msg: Any) -> None:
print(f"LOG: {msg}")
# NoReturn: funções que nunca retornam
def erro_fatal(mensagem: str) -> NoReturn:
raise SystemExit(mensagem)
# Literal: valores exatos como tipos
def configurar_modo(modo: Literal["dev", "prod", "test"]) -> None:
print(f"Modo configurado: {modo}")
# Uso correto
configurar_modo("dev") # OK
# configurar_modo("staging") # Erro de tipo!
Literal é particularmente útil para APIs com opções limitadas e para melhorar a legibilidade de código que usa strings mágicas.
7. Erros comuns e boas práticas
Erro 1: Uso excessivo de Union
# Ruim: Union de muitos tipos
def processar(valor: int | str | list | dict | None) -> None: ...
# Melhor: sobrecarga de funções ou classes separadas
from typing import overload
@overload
def processar(valor: int) -> None: ...
@overload
def processar(valor: str) -> None: ...
def processar(valor):
# Implementação
...
Erro 2: Confundir Generic com herança
# Ruim: usar Generic sem necessidade real
class MeuInt(Generic[int]): # Não faz sentido!
pass
# Correto: Generic para estruturas que armazenam/processam tipos variáveis
class Repositorio(Generic[T]):
def salvar(self, item: T) -> None: ...
def buscar(self, id: int) -> T: ...
Ferramentas complementares:
- mypy: análise estática de tipos
- pydantic: validação em runtime, especialmente útil com Literal e Union
from pydantic import BaseModel
from typing import Literal
class Config(BaseModel):
ambiente: Literal["dev", "prod", "test"]
timeout: int = 30
# Validação em runtime
config = Config(ambiente="dev") # OK
# config = Config(ambiente="staging") # ValidationError!
8. Conclusão e próximos passos
Dominar Union, Optional e Generic é essencial para escrever código Python seguro, reutilizável e bem documentado. Union lida com múltiplos tipos, Optional com valores ausentes e Generic com abstrações parametrizáveis. Combinados com TypeVar, Literal e as ferramentas de análise estática, esses recursos transformam Python em uma linguagem adequada para projetos de qualquer escala.
Para aprofundamento, estude as PEPs fundamentais (PEP 483, PEP 484, PEP 695) e explore ferramentas como mypy para validação estática — tema do próximo artigo desta série.
Referências
- PEP 484 – Type Hints — Documentação oficial que introduziu os type hints em Python, incluindo Union, Optional e Generic
- Python typing documentation — Documentação oficial do módulo
typing, com exemplos detalhados de todos os tipos avançados - mypy documentation – Generics — Guia completo sobre generics no mypy, incluindo TypeVar, variância e boas práticas
- Pydantic documentation – Types — Documentação sobre validação de tipos em runtime com pydantic, incluindo Union, Optional e Literal
- Real Python – Python Type Checking Guide — Tutorial prático sobre type hints em Python, cobrindo desde básico até tópicos avançados como Generic e Protocol
- PEP 695 – Type Parameter Syntax — PEP que introduziu a nova sintaxe concisa para TypeVar e Generic em Python 3.12