Python Admin

Dominando Mypy em Python: Verificação Estática de Tipos no Projeto Real em Projetos Reais Já leu

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,

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.

Referências


Artigos relacionados