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: intsã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 aUnion[Customer, None].- Se você chamar
customer.emailsem verificarNoneprimeiro, mypy reportará o erroItem "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:floate depoisint.Dict[str, float]é um dicionário com chaves do tipostre valores do tipofloat.
Observação: a partir de Python 3.9, você pode usar diretamente
list[float],dict[str, float]sem precisar importar detyping. Usetyping.List/typing.Dictprincipalmente 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 queT = 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 comoAny.
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 = Trueforça que todas as funções tenham type hints, evitando omissões.warn_return_any = Trueavisa quando uma função retornaAnysilenciosamente.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 retornarCustomer, 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
Anyem excesso: declarardef process(data: Any) -> Anyremove 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 comignore_missing_importsou instalandotypes-<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.