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