Introdução ao Mypy: Por Que Type Checking Importa
Quando você trabalha em projetos Python de médio a grande porte, a falta de verificação de tipos pode se tornar um pesadelo. Python é dinamicamente tipado, o que significa que os tipos são verificados apenas em tempo de execução. Isso é ótimo para prototipagem rápida, mas desastroso para produção quando um erro de tipo chega ao usuário final.
Mypy é uma ferramenta de verificação estática de tipos que analisa seu código antes da execução, identificando incompatibilidades de tipo sem rodar uma única linha. Ele usa as type hints — anotações de tipo que você adiciona ao código — para garantir que você está usando funções, variáveis e objetos corretamente. Não é um validador de lógica, mas é extremamente eficaz em pegar bugs silenciosos causados por tipos incorretos.
Fundamentos: Type Hints e Anotações de Tipo
O que são Type Hints?
Type hints são simplesmente anotações que informam qual tipo de dado uma variável, parâmetro ou retorno de função deve ter. Elas são completamente opcionais em Python — o código roda normalmente sem elas — mas são essenciais para o Mypy funcionar.
Vamos começar simples:
# Sem type hints (código válido, mas sem informação de tipo)
def saudacao(nome):
return f"Olá, {nome}!"
# Com type hints (código idêntico, mas com informação)
def saudacao(nome: str) -> str:
return f"Olá, {nome}!"
No segundo caso, estamos dizendo: "o parâmetro nome deve ser uma string, e a função retorna uma string". Se alguém chamar saudacao(123), o Mypy vai reclamar, mesmo que Python permita.
Tipos Básicos e Estruturas Complexas
Os tipos mais comuns vêm direto de Python: int, str, bool, float. Mas frequentemente você trabalha com coleções e estruturas mais complexas. Para isso, usamos o módulo typing:
from typing import List, Dict, Optional, Union, Tuple
# Lista de inteiros
numeros: List[int] = [1, 2, 3, 4]
# Dicionário com chaves string e valores inteiros
idades: Dict[str, int] = {"Alice": 30, "Bob": 25}
# Tipo que pode ser None (None é um tipo válido)
resultado: Optional[str] = None
# Um tipo ou outro
status: Union[int, str] = "ativo"
# Tupla com tipos específicos
coordenadas: Tuple[float, float] = (10.5, 20.3)
Quando você usa Optional[str], está dizendo que o valor pode ser uma string ou None. Sem essa anotação, código como valor = funcao_que_retorna_none() e depois print(valor.upper()) passaria despercebido e quebraria em produção.
Mypy na Prática: Configuração e Execução
Instalação e Setup Básico
Mypy é uma ferramenta independente do Python. Instale via pip:
pip install mypy
Depois, execute sobre seu código:
mypy seu_arquivo.py
Para verificar um projeto inteiro:
mypy .
Mypy criará um arquivo .mypy_cache/ para otimizar futuras análises. É seguro adicionar isso ao .gitignore.
Configuração com pyproject.toml
Para projetos profissionais, você quer configurar regras padrão. Crie um arquivo pyproject.toml na raiz:
[tool.mypy]
python_version = "3.10"
warn_return_any = true
warn_unused_configs = true
disallow_untyped_defs = true
disallow_incomplete_defs = true
check_untyped_defs = true
no_implicit_optional = true
warn_redundant_casts = true
warn_unused_ignores = true
warn_no_return = true
strict = true
Essas configurações ativam o modo "strict", que é a forma mais rigorosa. disallow_untyped_defs = true força você a anotar todas as funções. Isso parece restritivo, mas é exatamente o que projetos sérios precisam.
Um Exemplo Real: Sistema de Cadastro
Vamos criar um exemplo realista com problemas que Mypy detecta:
from typing import List, Optional
from dataclasses import dataclass
@dataclass
class Usuario:
id: int
nome: str
email: str
idade: Optional[int] = None
class Banco:
def __init__(self) -> None:
self.usuarios: List[Usuario] = []
def adicionar(self, usuario: Usuario) -> None:
"""Adiciona um usuário ao banco."""
self.usuarios.append(usuario)
def buscar_por_id(self, id: int) -> Optional[Usuario]:
"""Retorna o usuário com o ID especificado ou None."""
for usuario in self.usuarios:
if usuario.id == id:
return usuario
return None
def listar_emails(self) -> List[str]:
"""Retorna lista de todos os emails."""
return [u.email for u in self.usuarios]
# Uso correto
banco = Banco()
novo_usuario = Usuario(id=1, nome="Alice", email="alice@example.com", idade=30)
banco.adicionar(novo_usuario)
usuario_encontrado = banco.buscar_por_id(1)
if usuario_encontrado:
print(usuario_encontrado.nome)
Se você tentar fazer algo errado, Mypy avisa:
# ERRO: usuario_encontrado pode ser None, não posso acessar .nome direto
usuario_encontrado = banco.buscar_por_id(999)
print(usuario_encontrado.nome) # ❌ Mypy: Object of type "None" has no attribute "nome"
# ERRO: passando inteiro onde string é esperado
banco.adicionar(Usuario(id="abc", nome="Bob", email=123)) # ❌ Mypy: Argument 1 to "Usuario" has incompatible type
# CORRETO: tratamento adequado
usuario_encontrado = banco.buscar_por_id(1)
if usuario_encontrado is not None:
print(usuario_encontrado.nome) # ✅ Mypy: OK, usuario_encontrado é definitivamente Usuario aqui
Padrões Avançados: Generics, Protocolos e Type Aliases
Generics: Escrevendo Código Reutilizável com Tipos
Muitas vezes você quer escrever funções ou classes que funcionam com qualquer tipo, mas ainda quer segurança de tipos. Para isso, usamos type variables:
from typing import TypeVar, List, Generic
T = TypeVar('T') # Um tipo genérico que pode ser qualquer coisa
def primeira_elemento(lista: List[T]) -> Optional[T]:
"""Retorna o primeiro elemento da lista, ou None se vazia."""
if lista:
return lista[0]
return None
# Mypy entende que se você passa List[int], retorna Optional[int]
resultado_int = primeira_elemento([1, 2, 3]) # tipo: Optional[int]
resultado_str = primeira_elemento(["a", "b"]) # tipo: Optional[str]
Você também pode criar classes genéricas:
from typing import Generic, TypeVar
T = TypeVar('T')
class Caixa(Generic[T]):
"""Uma caixa que armazena um item de qualquer tipo."""
def __init__(self, conteudo: T) -> None:
self.conteudo = conteudo
def obter(self) -> T:
return self.conteudo
# Mypy entende os tipos específicos
caixa_numero = Caixa(42)
valor = caixa_numero.obter() # tipo: int
caixa_texto = Caixa("Python")
texto = caixa_texto.obter() # tipo: str
Protocolos: Interfaces Estruturais
Às vezes você quer dizer "qualquer objeto que tenha esses métodos" sem se importar com herança. Isso é um Protocol:
from typing import Protocol
class Persistivel(Protocol):
"""Qualquer coisa que possa ser salva."""
def salvar(self) -> None: ...
class Documento:
def salvar(self) -> None:
print("Documento salvo em disco")
class Email:
def salvar(self) -> None:
print("Email arquivado")
def arquivar(item: Persistivel) -> None:
"""Aceita qualquer objeto com método salvar()."""
item.salvar()
# Funciona com Documento e Email sem herança explícita
arquivar(Documento())
arquivar(Email())
Type Aliases: Nomes Legíveis para Tipos Complexos
Quando você tem tipos complexos, crie aliases:
from typing import Dict, List, Tuple
# Tipo complexo sem alias (ilegível)
def processar_dados(dados: Dict[str, List[Tuple[int, str]]]) -> None:
pass
# Com alias (muito melhor)
Registro = Tuple[int, str]
Tabela = Dict[str, List[Registro]]
def processar_dados(dados: Tabela) -> None:
pass
Integração em Projetos Reais: CI/CD e Boas Práticas
Mypy no Pipeline de CI/CD
Adicione Mypy ao seu pipeline de testes. Exemplo com GitHub Actions:
name: Type Check
on: [push, pull_request]
jobs:
mypy:
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v3
- uses: actions/setup-python@v4
with:
python-version: "3.10"
- run: pip install mypy
- run: mypy src/
Agora todo PR que quebra os tipos será rejeitado automaticamente.
Suppressões Controladas
Às vezes você precisa ignorar um erro de tipo por uma razão legítima. Use # type: ignore:
# Ignorar um erro específico na linha
dados = json.loads(entrada) # type: ignore[arg-type]
# Ignorar toda a linha
funcao_antiga_sem_tipos() # type: ignore
# Ignorar um bloco inteiro
# mypy: ignore-errors
def codigo_legado():
pass
Use com moderação! Cada # type: ignore deve ter um comentário explicando o porquê.
Estratégia de Adoção Gradual
Se você herdou um codebase sem tipos, não precisa anotar tudo de uma vez. Use # mypy: allow-untyped-defs para arquivos específicos:
# mypy: allow-untyped-defs
# Este arquivo é legado. Anotaremos gradualmente.
def funcao_antiga(x):
return x * 2
def funcao_nova(valor: int) -> int: # Novas funções com tipos
return valor * 2
Conclusão
Você aprendeu que Mypy traz confiabilidade a projetos Python através de verificação estática de tipos, permitindo detectar bugs antes da execução sem adicionar overhead em runtime. Em segundo lugar, type hints não são apenas anotações cosméticas — eles documentam contratos de código e permitem que ferramentas como IDEs e Mypy façam seu trabalho. Por fim, a adoção de Mypy em projetos profissionais é progressiva: comece com o modo padrão, evolua para o modo strict, e integre ao CI/CD para garantir qualidade consistente.