mypy usa anotações para detectar chamadas incompatíveis antes da execução. Python continua dinâmico e roda mesmo com erros do analisador, portanto tipagem complementa testes, não os substitui.

Configurar no pyproject.toml

[tool.mypy]
python_version = "3.13"
warn_unused_ignores = true
disallow_untyped_defs = true
no_implicit_optional = true
from collections.abc import Iterable


def totalizar(valores: Iterable[int]) -> int:
    return sum(valores)

Execute mypy src. Em código antigo, selecione primeiro módulos de domínio e bibliotecas compartilhadas. Impeça novas funções sem anotação antes de tentar corrigir tudo de uma vez. O guia de type hints apresenta unions, generics e protocolos.

Tratar imports e exceções

Procure pacotes de stubs mantidos para bibliotecas sem tipos. Use # type: ignore[codigo] somente na linha necessária, com motivo, e ative warn_unused_ignores para remover exceções obsoletas. Não use Any apenas para silenciar a ferramenta.

Adicione o mesmo comando à CI e fixe a configuração no repositório. Para testar diferentes versões e ferramentas, o artigo sobre tox em Python mostra uma matriz reproduzível.

A documentação oficial do mypy, consultada em 22 de julho de 2026, recomenda adoção incremental para bases existentes e explica o modo estrito. O objetivo não é satisfazer o analisador a qualquer custo, mas tornar contratos importantes explícitos.

Entender o que o mypy verifica

mypy analisa valores anotados sem executar o programa. Ele pode identificar que uma função espera str, mas recebeu um int, acompanhar o tipo depois de um teste com isinstance e conferir se todos os caminhos devolvem o resultado declarado.

Essa garantia tem limites. JSON, variáveis de ambiente, banco de dados e respostas de rede ainda exigem validação em tempo de execução. Uma anotação não converte nem inspeciona dados externos. Testes continuam responsáveis por comportamento, integrações, tempo e efeitos colaterais. A análise estática torna contratos coerentes; a suíte confirma que a implementação cumpre esses contratos.

Any também forma uma fronteira. A maioria das operações sobre ele é aceita, portanto um valor impreciso pode carregar um erro por várias funções. Use Any quando uma interface realmente não tem tipos, valide ou converta o dado logo na entrada e devolva um tipo preciso ao restante da aplicação.

Criar uma linha de base prática

Mantenha a configuração versionada para que editor, terminal e CI usem as mesmas regras. No primeiro ciclo, limite a análise aos diretórios mantidos pela equipe:

[tool.mypy]
files = ["src", "tests"]
python_version = "3.13"
show_error_codes = true
warn_unused_ignores = true
warn_redundant_casts = true
check_untyped_defs = true

check_untyped_defs examina o corpo de funções sem anotações completas, mas não torna suas interfaces tipadas. disallow_untyped_defs é um passo posterior mais rigoroso. Em um projeto antigo, ative essa regra primeiro nos módulos novos ou já migrados.

Execute o comando na raiz do repositório. A descoberta da configuração e os caminhos de import podem mudar em outro diretório. Fixe versões do mypy e dos stubs nas dependências de desenvolvimento, pois atualizações do analisador podem revelar erros legítimos novos.

Migrar por fronteiras do sistema

Uma unidade de migração útil é um pacote coerente, não uma quantidade arbitrária de avisos. Comece por modelos de domínio e utilitários compartilhados, porque suas anotações melhoram a inferência dos consumidores. Tipifique funções públicas e avance para os helpers internos.

[[tool.mypy.overrides]]
module = "relatorios_legados.*"
disallow_untyped_defs = false

[[tool.mypy.overrides]]
module = "faturamento.*"
disallow_untyped_defs = true
disallow_any_generics = true

A exceção registra onde a dívida permanece e protege o código migrado. Evite ignore_errors global, que esconde regressões junto com o passivo inicial. Restrinja cada override e remova-o quando o pacote estiver pronto.

Não anote tudo como object ou Any apenas para zerar o relatório. object aceita poucas operações; Any aceita quase todas e transfere o risco. Um protocolo pode descrever exatamente o comportamento exigido pelo consumidor.

Modelar valores opcionais

str | None informa que ausência é um caso real. Isso é diferente de apenas oferecer um valor padrão. Faça a verificação explicitamente para estreitar o tipo:

def nome_exibicao(nome: str | None) -> str:
    if nome is None:
        return "Anônimo"
    return nome.strip()

Evite assert valor is not None se a ausência puder ocorrer de verdade. Prefira retornar, lançar uma exceção significativa ou validar o objeto durante sua criação. Uma asserção usada para convencer o analisador pode ocultar uma regra de negócio incompleta.

Predicados mais complexos podem usar TypeGuard, mas apenas quando verificam todas as condições prometidas pelo tipo estreito. mypy confia na declaração, portanto um guard incorreto é tão perigoso quanto um cast sem verificação.

Preservar tipos com generics e protocolos

Generics mantêm a relação entre entrada e saída. Uma função que devolve o primeiro item deve conservar o tipo do elemento:

from collections.abc import Sequence
from typing import TypeVar

T = TypeVar("T")


def primeiro(itens: Sequence[T]) -> T:
    if not itens:
        raise ValueError("itens não pode ser vazio")
    return itens[0]

Protocolos descrevem contratos estruturais. Uma função aceita qualquer objeto com os métodos necessários, sem obrigar classes da aplicação a herdar de uma base específica. Mantenha o protocolo pequeno e centrado na necessidade do consumidor. Protocolos extensos costumam indicar responsabilidades demais.

Use cast() apenas quando a lógica de execução já estabeleceu algo que o mypy não consegue inferir. O cast não converte nem verifica o valor. Se houver incerteza real, use isinstance, um parser ou validação de esquema.

Integrar bibliotecas sem tipos

Alguns pacotes distribuem tipos com o marcador py.typed; outros dependem de pacotes de stubs separados. Siga a orientação oficial da biblioteca e atualize versões compatíveis em conjunto. Um stub antigo pode discordar da API instalada.

Quando não houver tipos, prefira um adaptador local com interface precisa. Ele concentra a incerteza e facilita uma substituição futura. Uma exceção por módulo é mais segura que ignorar todos os imports:

[[tool.mypy.overrides]]
module = "fornecedor_sem_tipos.*"
ignore_missing_imports = true

Arquivos gerados podem receber uma exclusão explícita se forem reproduzíveis e nunca editados. Não exclua uma árvore inteira porque um único módulo gera ruído.

Investigar em vez de suprimir

Leia o código do diagnóstico e encontre o primeiro ponto em que o tipo ficou amplo demais. reveal_type(valor) ajuda durante a investigação e deve ser removido depois. Coleções vazias frequentemente precisam de anotação:

ids_usuarios: list[int] = []

Se uma exceção for inevitável, restrinja o diagnóstico: # type: ignore[import-untyped]. Explique o motivo quando ele não for óbvio. Com warn_unused_ignores, a CI avisa quando uma supressão se torna obsoleta.

Não responda a toda incompatibilidade com cast. A anotação pode ter revelado retorno None não documentado, mutação com valor errado ou uma função que promete aceitar mais do que suporta.

Tornar a CI previsível

A CI deve executar o mesmo comando local, com a mesma configuração e o mesmo conjunto de dependências. Um ambiente próprio no tox pode instalar mypy e stubs e chamar mypy src tests. Deixe o job falhar em diagnósticos e não masque o status do comando.

Na adoção gradual, fixe o escopo atual e impeça novos erros nele. Expandir um pacote por vez oferece um critério visível de conclusão. Comparar apenas a contagem total é frágil, pois um erro grave pode substituir vários avisos antigos enquanto o número diminui.

O editor dá retorno rápido, mas a CI é a referência compartilhada. Documente o comando e mantenha-o rápido. Rode testes junto com a análise: tipos consistentes não provam que o sistema em execução está correto.

Definir uma política sustentável

Exija anotações onde elas comunicam fronteiras estáveis: funções públicas, modelos compartilhados, callbacks e transformações de dados. Variáveis locais normalmente podem usar inferência, exceto quando uma coleção vazia ou uma interface ampla precisa de intenção explícita.

Revise mudanças de tipo como decisões de design. Uma union difícil de entender pode revelar responsabilidades misturadas; verificações opcionais repetidas podem apontar um estado intermediário inválido; Any recorrente pode indicar entrada sem validação. O resultado valioso não é apenas um terminal sem erros, mas contratos que pessoas e ferramentas conseguem compreender.