Type hints em Python e verificação com mypy

1. Visão geral

Python é uma linguagem com tipagem dinâmica, o que significa que você não precisa declarar tipos de dados previamente. Isso é flexível, mas pode causar erros em tempo de execução quando o projeto cresce. Type hints (PEP 484) permitem que você anotasse tipos de dados sem alterar o comportamento em tempo de execução, enquanto mypy lerá essas anotações para detectar erros de tipo logo quando você escreve o código.

Após ler este artigo, você saberá como escrever type hints para variáveis, funções, classes, usar Optional, Union, TypedDict, genéricos, e integrar mypy no seu processo de verificação de código.

2. Type hints básicos

2.1. Declarar tipos para variáveis e funções

def calculate_total(price: float, quantity: int) -> float:
    return price * quantity

customer_name: str = "Nguyễn Văn A"
total: float = calculate_total(price=15.5, quantity=3)
print(total)
  • price: float, quantity: int são os tipos dos parâmetros de entrada.
  • -> float é o tipo do valor retornado.
  • customer_name: str é uma anotação para a variável, ajudando a IDE a sugerir e mypy verificar.
46.5

Observação: type hints NÃO forçam tipos em tempo de execução. Se você chamar calculate_total("10", 2), Python ainda vai executar normalmente porque o interpretador ignora a anotação — apenas ferramentas como mypy relatam o erro.

2.2. Instalando mypy

pip install mypy

Verificar um arquivo:

mypy order_service.py

Se não houver erros, mypy imprime:

Success: no issues found in 1 source file

3. Optional e Union para valores que podem ser None

Muitas funções retornam None quando não encontram dados. Use Optional para indicar isso explicitamente.

from typing import Optional

class Customer:
    def __init__(self, customer_id: int, email: str) -> None:
        self.customer_id = customer_id
        self.email = email

def find_customer(customer_id: int) -> Optional[Customer]:
    database = {1: Customer(1, "a@example.com")}
    return database.get(customer_id)

customer = find_customer(2)
if customer is not None:
    print(customer.email)
  • Optional[Customer] é equivalente a Union[Customer, None].
  • Se você chamar customer.email sem verificar None primeiro, mypy reportará o erro Item "None" of "Optional[Customer]" has no attribute "email".
from typing import Union

def format_id(value: Union[int, str]) -> str:
    return str(value).zfill(6)

Union[int, str] permite que o parâmetro seja int ou str. A partir de Python 3.10, você pode escrever de forma mais concisa usando int | str em vez de Union[int, str], e Customer | None em vez de Optional[Customer].

4. Tipos de coleção: list, dict, tuple

from typing import Dict, List, Tuple

def summarize_orders(order_totals: List[float]) -> Tuple[float, int]:
    return sum(order_totals), len(order_totals)

def build_price_table(items: Dict[str, float]) -> Dict[str, float]:
    return {name: price * 1.1 for name, price in items.items()}
  • List[float] significa uma lista de números reais.
  • Tuple[float, int] significa uma tupla com exatamente 2 elementos: float e depois int.
  • Dict[str, float] é um dicionário com chaves do tipo str e valores do tipo float.

Observação: a partir de Python 3.9, você pode usar diretamente list[float], dict[str, float] sem precisar importar de typing. Use typing.List/typing.Dict principalmente para compatibilidade com Python 3.8 ou inferior.

5. TypedDict para dados em forma de dicionário estruturado

Quando você trabalha com JSON ou dicionários com esquema fixo, TypedDict ajuda mypy a verificar cada chave.

from typing import TypedDict

class OrderPayload(TypedDict):
    order_id: int
    customer_email: str
    total: float

def create_order(payload: OrderPayload) -> str:
    return f"Order #{payload['order_id']} - {payload['customer_email']} - {payload['total']}"

payload: OrderPayload = {
    "order_id": 101,
    "customer_email": "b@example.com",
    "total": 250.0,
}
print(create_order(payload))

Se você esquecer a chave total ou digitar errado "totl", mypy relata:

error: Missing key "total" for TypedDict "OrderPayload"

Esta é uma grande vantagem em relação a usar Dict[str, Any] — você controla a estrutura dos dados mantendo um dicionário Python puro em tempo de execução.

6. Genéricos com TypeVar

Ao escrever funções ou classes que funcionam com múltiplos tipos de dados, você usa TypeVar para manter a relação entre tipos de entrada e saída.

from typing import TypeVar, List

T = TypeVar("T")

def first_item(items: List[T]) -> T:
    return items[0]

first_name: str = first_item(["Hùng", "Lan"])
first_number: int = first_item([10, 20, 30])
  • TypeVar("T") cria um placeholder de tipo.
  • Quando você chama first_item(["Hùng", "Lan"]), mypy infere que T = str, então o resultado retornado é str.
  • Quando você chama com List[int], T = int. Dessa forma mypy sabe exatamente qual é o tipo retornado em cada chamada, em vez de ser genérico como Any.

7. Classes e Protocol no lugar de interfaces

Python não possui interfaces como Java, mas Protocol (PEP 544) permite que você defina structural typing — verificar “tem o método correto?” em vez de forçar herança.

from typing import Protocol

class PaymentGateway(Protocol):
    def charge(self, amount: float) -> bool:
        ...

class VnPayGateway:
    def charge(self, amount: float) -> bool:
        print(f"Charging {amount} via VNPay")
        return True

def process_payment(gateway: PaymentGateway, amount: float) -> bool:
    return gateway.charge(amount)

process_payment(VnPayGateway(), 199000.0)

VnPayGateway não precisa herdar de PaymentGateway — apenas precisa ter o método charge(self, amount: float) -> bool e mypy vai aceitar que ela satisfaz o Protocol. Esta é uma abordagem muito “Pythônica”, mantendo duck typing mas ainda tendo static check.

8. Configurando mypy para o seu projeto

Adicione a dependência em requirements-dev.txt:

mypy==1.11.2

Crie um arquivo de configuração mypy.ini no diretório raiz:

[mypy]
python_version = 3.12
disallow_untyped_defs = True
warn_return_any = True
warn_unused_ignores = True
ignore_missing_imports = True
  • disallow_untyped_defs = True força que todas as funções tenham type hints, evitando omissões.
  • warn_return_any = True avisa quando uma função retorna Any silenciosamente.
  • ignore_missing_imports = True é útil ao usar bibliotecas de terceiros que ainda não possuem type stubs.

Executar verificação em todo o projeto:

mypy src/

9. Escrever testes para comportamento com type hints

Type hints não substituem testes, mas ajudam a deixar os testes mais claros sobre tipos de entrada e saída. Use pytest:

<!-- requirements-dev.txt em formato equivalente, apenas para ilustrar a estrutura de dependência -->
pytest==8.3.2
from order_service import calculate_total

def test_calculate_total_returns_correct_sum() -> None:
    result = calculate_total(price=20.0, quantity=5)

    assert result == 100.0

def test_calculate_total_with_zero_quantity() -> None:
    result = calculate_total(price=50.0, quantity=0)

    assert result == 0.0

Executar testes:

pytest -v
test_order_service.py::test_calculate_total_returns_correct_sum PASSED
test_order_service.py::test_calculate_total_with_zero_quantity PASSED

Você pode combinar ambas as etapas em CI: executar mypy primeiro para capturar erros de tipo, depois executar pytest para verificar a lógica de negócios. As duas ferramentas se complementam — mypy captura “tipo de dados incorreto”, pytest captura “lógica incorreta”.

10. Erros comuns e quando você não precisa de mypy

  • Esquecer de atualizar type hints quando alterar lógica: por exemplo, uma função muda de retornar Optional[Customer] para sempre retornar Customer, mas você esquece de atualizar a anotação — mypy não vai relatar erro se o código ainda corresponder ao tipo anterior mais amplo, resultando em informação de anotação imprecisa.
  • Usar Any em excesso: declarar def process(data: Any) -> Any remove a capacidade de verificação de mypy, desabilitando efetivamente static typing ali.
  • Anotação incorreta com bibliotecas de terceiros sem stubs: muitos pacotes antigos ainda não publicaram type stubs, mypy vai relatar error: Cannot find implementation or library stub. Você resolve com ignore_missing_imports ou instalando types-<package> se estiver disponível.
  • Quando NÃO usar mypy rigorosamente: com scripts pequenos, scripts de uso único (throwaway script), ou prototipagem rápida para testar uma ideia — forçar type hints completos pode desacelerar sua velocidade de escrita sem trazer valor proporcional.

Observação: mypy apenas faz análise estática (static analysis) no código fonte, não executa o programa. Ele não consegue capturar erros que dependem de dados em tempo de execução, por exemplo, valores lidos de um arquivo de configuração ou API externa que têm o tipo esperado ou não.

11. Conclusão

Type hints tornam o código Python mais legível, fácil de manter e reduzem erros por confusão de tipos, enquanto mypy transforma essas anotações em uma verdadeira ferramenta de verificação em vez de apenas documentação. Você deve começar a aplicar gradualmente em funções públicas, módulos importantes, depois expandir gradualmente por todo o projeto usando a configuração disallow_untyped_defs. Integrar mypy em CI junto com testes usando pytest é a forma mais prática de manter a flexibilidade do Python enquanto ganha segurança próxima à de linguagens com tipagem estática.

pythontype-hintsmypystatic-typing