mypy: validação estática de tipos

1. Introdução ao mypy e seus Fundamentos

Python é tradicionalmente uma linguagem de tipagem dinâmica, onde os tipos das variáveis são verificados apenas em tempo de execução. Isso traz flexibilidade, mas também abre espaço para erros difíceis de detectar. O mypy surge como uma ferramenta de validação estática de tipos que analisa seu código antes da execução, identificando inconsistências de tipo que poderiam causar falhas em produção.

A diferença fundamental é simples: enquanto o interpretador Python descobre erros de tipo quando executa o código, o mypy os encontra durante o desenvolvimento. Isso economiza horas de debugging e torna o código mais auto-documentado.

Para instalar o mypy, use:

pip install mypy

Crie um arquivo exemplo.py:

def saudacao(nome: str) -> str:
    return f"Olá, {nome}!"

resultado = saudacao(42)  # Erro: int em vez de str

Execute a verificação:

mypy exemplo.py

O mypy apontará o erro: Argument 1 to "saudacao" has incompatible type "int"; expected "str".

Para uma verificação mais rigorosa, ative o modo strict:

mypy --strict exemplo.py

O modo strict habilita automaticamente flags como --disallow-untyped-defs (exige tipos em todas as funções) e --warn-return-any (alerta quando uma função retorna Any).

2. Configuração de Projeto com mypy

Em projetos maiores, usar flags no terminal se torna impraticável. A solução é criar um arquivo de configuração. O mypy suporta mypy.ini e pyproject.toml.

Exemplo de mypy.ini:

[mypy]
strict = True
disallow_untyped_defs = True
check_untyped_defs = True
warn_return_any = True

[mypy-module.legacy_code.*]
ignore_errors = True

O mesmo em pyproject.toml:

[tool.mypy]
strict = true
disallow_untyped_defs = true
check_untyped_defs = true
warn_return_any = true

[[tool.mypy.overrides]]
module = "legacy_code.*"
ignore_errors = true

A seção [mypy-module.*] permite configurar módulos específicos. Por exemplo, você pode exigir tipos estritos no código novo enquanto ignora módulos legados.

3. Type Hints Essenciais para Validação

Os type hints básicos são diretos:

from typing import List, Dict, Optional, Union, Any

idade: int = 30
nome: str = "Alice"
altura: float = 1.75
ativo: bool = True

# Coleções
notas: List[int] = [8, 9, 7]
contatos: Dict[str, int] = {"Alice": 1234, "Bob": 5678}

# Opcionais e União
endereco: Optional[str] = None  # Equivalente a Union[str, None]
valor: Union[int, float] = 3.14
generico: Any = "qualquer coisa"

Type aliases e NewType oferecem mais expressividade:

from typing import NewType

# Type alias
Coordenada = tuple[float, float]

# NewType - cria um subtipo distinto
UserId = NewType('UserId', int)
ProductId = NewType('ProductId', int)

def buscar_usuario(id: UserId) -> str:
    return f"Usuário {id}"

# Erro: ProductId não é intercambiável com UserId
buscar_usuario(ProductId(5))  # mypy apontará erro

4. Validação de Funções e Métodos

Funções tipadas são a espinha dorsal da validação estática:

def calcular_media(notas: List[float]) -> float:
    if not notas:
        return 0.0
    return sum(notas) / len(notas)

Para sobrecarga de funções, use @overload:

from typing import overload

@overload
def processar(valor: int) -> str: ...

@overload
def processar(valor: str) -> int: ...

def processar(valor: int | str) -> str | int:
    if isinstance(valor, int):
        return str(valor)
    return len(valor)

Callables permitem tipar funções de ordem superior:

from typing import Callable

def aplicar_operacao(
    valores: List[int],
    operacao: Callable[[int], int]
) -> List[int]:
    return [operacao(v) for v in valores]

def dobrar(x: int) -> int:
    return x * 2

resultado = aplicar_operacao([1, 2, 3], dobrar)

5. Validação de Classes e Herança

Classes também se beneficiam da tipagem estática:

class ContaBancaria:
    def __init__(self, titular: str, saldo_inicial: float = 0.0) -> None:
        self.titular = titular
        self.saldo = saldo_inicial

    @property
    def saldo_formatado(self) -> str:
        return f"R$ {self.saldo:.2f}"

    def depositar(self, valor: float) -> None:
        if valor <= 0:
            raise ValueError("Valor deve ser positivo")
        self.saldo += valor

Genéricos em classes permitem criar componentes reutilizáveis:

from typing import Generic, TypeVar

T = TypeVar('T')

class Pilha(Generic[T]):
    def __init__(self) -> None:
        self._itens: list[T] = []

    def push(self, item: T) -> None:
        self._itens.append(item)

    def pop(self) -> T:
        return self._itens.pop()

pilha_int: Pilha[int] = Pilha()
pilha_int.push(10)
valor = pilha_int.pop()  # tipo inferido como int

6. Integração com Código Legado e Bibliotecas Externas

Para bibliotecas sem type hints, o mypy usa stubs (arquivos .pyi). Muitas bibliotecas populares já possuem stubs mantidos pela comunidade:

pip install types-requests types-Pillow types-python-dateutil

Para migração gradual, use estratégias como:

from typing import Any

# Ignorar verificação em uma linha específica
resultado = funcao_legada()  # type: ignore

# Usar Any para contornar tipos desconhecidos
def interface_com_legado(param: Any) -> Any:
    return processamento_antigo(param)

7. Ferramentas Avançadas e Plugins

Plugins estendem o mypy para frameworks específicos:

pip install django-stubs pydantic mypy

Exemplo com Pydantic:

from pydantic import BaseModel

class Usuario(BaseModel):
    nome: str
    idade: int
    email: str

# mypy valida que os tipos estão corretos
usuario = Usuario(nome="Alice", idade=30, email="alice@exemplo.com")

Para integração com IDEs, configure o PyCharm (Settings → Tools → External Tools) ou VS Code (extensão Python + mypy) para executar o mypy automaticamente ao salvar arquivos.

8. Padrões Comuns e Boas Práticas

Tratamento adequado de None:

def buscar_usuario(id: int) -> Optional[str]:
    if id == 0:
        return None
    return f"Usuário {id}"

# Boa prática: usar assert ou isinstance
usuario = buscar_usuario(1)
assert usuario is not None  # mypy entende que não é None após o assert
print(usuario.upper())

Depuração com reveal_type:

from typing import reveal_type

valor = [1, 2, 3]
reveal_type(valor)  # mypy mostra: list[int]

Evite armadilhas com tipos recursivos:

from typing import List, Optional

# Tipo recursivo para árvore binária
class No:
    def __init__(
        self,
        valor: int,
        esquerda: Optional['No'] = None,
        direita: Optional['No'] = None
    ) -> None:
        self.valor = valor
        self.esquerda = esquerda
        self.direita = direita

O mypy é uma ferramenta indispensável para projetos Python que buscam qualidade, manutenibilidade e menos bugs em produção. Comece gradualmente, configure corretamente seu projeto e veja a diferença que a validação estática de tipos pode fazer.

Referências