Tipagem estática: type hints básicos

1. Introdução aos Type Hints

Python sempre foi conhecido por sua tipagem dinâmica — você pode atribuir qualquer valor a qualquer variável sem declarações de tipo. No entanto, isso pode tornar o código difícil de entender e manter em projetos grandes. Type hints (ou anotações de tipo) são uma maneira de indicar os tipos esperados de variáveis, parâmetros e retornos de funções, sem alterar o comportamento em tempo de execução.

Os benefícios incluem:
- Legibilidade: outros desenvolvedores (e você no futuro) entendem imediatamente o que uma função espera e retorna.
- Manutenção: ferramentas detectam inconsistências de tipo antes mesmo de executar o código.
- Ferramentas: IDEs oferecem autocompletar, refatoração e detecção de erros mais precisas.

É importante entender que type hints são dicas, não imposições. Python continua sendo uma linguagem dinâmica — a verificação real é feita por ferramentas externas como mypy.

Historicamente, a PEP 484 (2014) introduziu a sintaxe formal de type hints, e desde Python 3.5 eles são suportados oficialmente. A partir do Python 3.9, a sintaxe foi simplificada com tipos nativos como list[int] em vez de List[int] do módulo typing.

2. Sintaxe Básica de Anotações de Tipo

A sintaxe mais simples é anotar variáveis com : seguido do tipo:

nome: str = "Alice"
idade: int = 30
altura: float = 1.75
ativo: bool = True
valor_nulo: None = None

Para funções, anotamos parâmetros e o retorno com ->:

def saudacao(nome: str) -> str:
    return f"Olá, {nome}!"

def soma(a: int, b: int) -> int:
    return a + b

def processar(valor: float) -> None:
    print(f"Processando {valor}")

Os tipos nativos disponíveis são: int, float, str, bool, None, bytes, complex.

3. Tipos de Coleções

Coleções podem ser anotadas com os tipos genéricos. Em Python 3.9+, use a sintaxe nativa:

# Listas
numeros: list[int] = [1, 2, 3]
matriz: list[list[int]] = [[1, 2], [3, 4]]

# Tuplas
coordenada: tuple[float, float] = (10.5, 20.3)
dados_mistos: tuple[str, int, bool] = ("abc", 42, True)

# Dicionários
notas: dict[str, float] = {"João": 8.5, "Maria": 9.0}
config: dict[str, dict[str, int]] = {"tela": {"largura": 1920, "altura": 1080}}

# Conjuntos
tags: set[str] = {"python", "tipagem", "hints"}
imutavel: frozenset[int] = frozenset([1, 2, 3])

Em Python 3.8 e anteriores, você precisa importar List, Dict, etc. do módulo typing:

from typing import List, Dict, Tuple

numeros: List[int] = [1, 2, 3]
notas: Dict[str, float] = {"João": 8.5}

A diferença é apenas sintática — ambas funcionam, mas a sintaxe nativa é mais limpa e recomendada para Python 3.9+.

4. Tipos Opcionais e Union

Muitas vezes, uma variável pode ser de um tipo ou None. Use Optional ou Union:

from typing import Optional, Union

# Optional[str] significa str ou None
def buscar_usuario(id: int) -> Optional[str]:
    if id == 1:
        return "Alice"
    return None

# Union permite múltiplos tipos
def converter(valor: Union[int, float, str]) -> float:
    if isinstance(valor, str):
        return float(valor)
    return float(valor)

# Sintaxe moderna (Python 3.10+): X | Y
def processar(valor: int | float | None) -> None:
    if valor is not None:
        print(valor * 2)

Boas práticas:
- Use Optional[X] quando o único tipo alternativo for None.
- Use Union[X, Y, Z] para múltiplos tipos possíveis.
- A partir do Python 3.10, X | Y | None é mais legível que Union[X, Y, None].

5. Tipos Especiais: Any, Callable, TypeVar Básico

Any é o tipo coringa — aceita qualquer valor e desativa a verificação:

from typing import Any

def log(mensagem: Any) -> None:
    print(mensagem)

dado: Any = 42
dado = "texto"  # Sem erro

Callable anota funções como parâmetros:

from typing import Callable

def executar(funcao: Callable[[int, int], int], a: int, b: int) -> int:
    return funcao(a, b)

def somar(x: int, y: int) -> int:
    return x + y

resultado = executar(somar, 3, 4)  # 7

TypeVar permite criar funções genéricas:

from typing import TypeVar

T = TypeVar('T')  # Pode ser qualquer tipo

def primeiro_elemento(lista: list[T]) -> T:
    return lista[0]

print(primeiro_elemento([1, 2, 3]))  # int
print(primeiro_elemento(["a", "b"]))  # str

NoReturn é para funções que nunca retornam (ex.: que sempre levantam exceção):

from typing import NoReturn

def erro_fatal() -> NoReturn:
    raise SystemExit("Erro crítico")

6. Type Hints com Classes e Objetos

Anote atributos de classe e métodos com self:

class Pessoa:
    nome: str
    idade: int

    def __init__(self, nome: str, idade: int) -> None:
        self.nome = nome
        self.idade = idade

    def aniversario(self) -> None:
        self.idade += 1

    def apresentar(self) -> str:
        return f"Olá, sou {self.nome} e tenho {self.idade} anos"

Em Python 3.11+, use Self para indicar que um método retorna a própria instância:

from typing import Self

class Animal:
    def __init__(self, nome: str) -> None:
        self.nome = nome

    def clone(self) -> Self:  # Retorna o mesmo tipo da subclasse
        return type(self)(self.nome)

class Cachorro(Animal):
    def latir(self) -> str:
        return "Au au!"

rex = Cachorro("Rex")
clone_rex = rex.clone()  # type: Cachorro
print(clone_rex.latir())  # Funciona!

7. Ferramentas e Verificação Estática

O mypy é o verificador de tipos mais popular. Instale com:

pip install mypy

Execute sobre seu código:

mypy meu_arquivo.py

Exemplo de erros detectados:

def dobro(x: int) -> int:
    return x * 2

resultado = dobro("texto")  # mypy detecta erro: Argument 1 to "dobro" has incompatible type "str"; expected "int"

Para ignorar uma linha específica:

valor: int = "42"  # type: ignore

Configure o mypy com um arquivo mypy.ini:

[mypy]
python_version = 3.11
strict = True

Ou no pyproject.toml:

[tool.mypy]
python_version = "3.11"
strict = true

8. Boas Práticas e Armadilhas Comuns

Quando anotar:
- Sempre anote funções públicas e APIs.
- Anote variáveis complexas ou quando o tipo não é óbvio.
- Evite anotar variáveis locais triviais (for i in range(10) não precisa de i: int).

Evite Any em excesso — ele desativa a verificação e anula os benefícios:

# Ruim
def processar(dados: Any) -> Any:
    return dados.upper()  # Pode falhar em runtime

# Bom
def processar(dados: str) -> str:
    return dados.upper()

Cuidado com tipos mutáveis em parâmetros padrão:

# Ruim - mypy não detecta, mas é perigoso
def adicionar(item: str, lista: list[str] = []) -> list[str]:
    lista.append(item)
    return lista

# Bom
def adicionar(item: str, lista: list[str] | None = None) -> list[str]:
    if lista is None:
        lista = []
    lista.append(item)
    return lista

Documentação vs type hints: eles são complementares. Type hints dizem o que uma função espera/retorna; a docstring explica como e por quê.

def calcular_media(notas: list[float]) -> float:
    """
    Calcula a média aritmética de uma lista de notas.

    Args:
        notas: Lista de valores numéricos (0 a 10).

    Returns:
        Média calculada.
    """
    return sum(notas) / len(notas)

Lembre-se: type hints são uma ferramenta poderosa para tornar seu Python mais robusto, legível e fácil de manter. Comece anotando funções críticas e evolua gradualmente.

Referências