Python é uma linguagem de tipagem dinâmica, o que significa que os tipos das variáveis são inferidos em tempo de execução. Isso traz flexibilidade, mas também pode levar a erros sutis que só aparecem quando o programa é executado. O type checking (verificação de tipos) estático surge como uma solução para mitigar esses problemas, permitindo que você detecte erros de tipo antes mesmo de rodar o código. Nesta aula, vamos explorar a ferramenta mypy, o conceito de tipos graduais, uma visão geral de generics e os benefícios de adotar essa prática no seu desenvolvimento.

Vamos começar entendendo como o type checking se encaixa no ecossistema Python e por que ele é importante, mesmo em uma linguagem dinamicamente tipada. Em seguida, mergulharemos em exemplos práticos com mypy e veremos como ele pode ser integrado ao seu fluxo de trabalho.

mypy

mypy é o verificador de tipos estático mais popular para Python. Ele analisa o código-fonte e verifica se as anotações de tipo são consistentes, apontando possíveis erros. O mypy foi criado por Jukka Lehtosalo e é um projeto da Dropbox, amplamente adotado na comunidade. Ele suporta tanto a sintaxe de anotações de tipo do Python 3 (PEP 484) quanto a sintaxe de comentários para versões mais antigas.

Para instalar o mypy, você pode usar o pip:

pip install mypy

Considere o seguinte exemplo simples:

def saudacao(nome: str) -> str:
    return "Olá, " + nome

print(saudacao("Maria"))  # OK
print(saudacao(42))        # Erro de tipo

Ao executar mypy nesse arquivo, ele reportará o erro na segunda chamada, pois 42 não é uma string. Isso é feito sem executar o código, apenas analisando a árvore sintática.

O mypy é altamente configurável. Você pode criar um arquivo mypy.ini ou pyproject.toml para definir configurações, como o nível de rigor, quais módulos ignorar, etc. Por exemplo, para permitir importações não tipadas, você pode usar a opção ignore_missing_imports.

O mypy também suporta a verificação incremental, o que o torna rápido em projetos grandes. Ele pode ser integrado a editores de código e sistemas de CI, garantindo que os erros de tipo sejam capturados automaticamente.

Tipos graduais

O conceito de tipos graduais (gradual typing) permite que você adicione tipos ao seu código de forma incremental, sem a necessidade de tipar todo o código de uma vez. Isso é fundamental para a adoção em projetos existentes, onde você pode começar a adicionar anotações em partes críticas sem reescrever tudo.

Em Python, isso é possível porque as anotações de tipo são opcionais. Se você não anota uma variável, ela é tratada como do tipo Any (qualquer). O mypy permite que você controle o nível de rigor: se você definir disallow_untyped_defs = true, ele exigirá que todas as funções tenham anotações, mas você pode deixar isso desligado para permitir a migração gradual.

Um exemplo de tipos graduais na prática:

def processar(item):  # Sem anotação - tipo Any
    return item.upper()

# Com anotação gradual
from typing import Any

def processar(item: Any) -> str:
    return item.upper()

No primeiro caso, o mypy não sabe o tipo de item e não reportará erros, mas também não oferece proteção. No segundo, você explicitamente declara Any, o que é equivalente, mas documenta a intenção.

O ideal é ir substituindo Any por tipos mais específicos conforme você entende melhor o código. O mypy pode ajudar a identificar onde você está usando Any e sugerir melhorias.

Generics (visão geral)

Generics permitem que você crie funções, classes e estruturas de dados que funcionam com qualquer tipo, mantendo a segurança de tipos. Em Python, o módulo typing fornece classes como List[T], Dict[K, V], Optional[T], e também permite criar seus próprios generics usando TypeVar.

Um exemplo clássico é uma função que retorna o primeiro elemento de uma lista:

from typing import TypeVar, List

T = TypeVar("T")

def primeiro_elemento(lista: List[T]) -> T:
    return lista[0]

print(primeiro_elemento([1, 2, 3]))  # int
print(primeiro_elemento(["a", "b"]))  # str

Aqui, T é uma variável de tipo que será vinculada ao tipo dos elementos da lista. Isso garante que o retorno seja do mesmo tipo dos elementos, permitindo que o mypy verifique consistência.

Você também pode criar classes genéricas:

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 = Pilha[int]()
pilha.push(1)
# pilha.push("a")  # erro de tipo

Generics são essenciais para criar bibliotecas reutilizáveis e seguras. Eles permitem que o código seja flexível sem sacrificar a verificação de tipos.

Benefícios

Adotar type checking traz diversos benefícios para o desenvolvimento de software em Python. Primeiro, a detecção precoce de erros: erros de tipo são capturados em tempo de desenvolvimento, não em produção, economizando tempo e recursos. Segundo, melhora a legibilidade e a documentação: as anotações de tipo servem como documentação executável, tornando o código mais claro para outros desenvolvedores.

Terceiro, facilita a refatoração: com tipos, você pode alterar o código com mais confiança, pois o verificador apontará quebras. Quarto, melhora a integração com IDEs: editores como VS Code e PyCharm usam as anotações para fornecer autocompletar e sugestões mais precisas. Quinto, ajuda em projetos grandes: a verificação estática ajuda a manter a consistência em bases de código extensas, especialmente em times.

Além disso, o type checking pode melhorar o desempenho em alguns casos, pois permite otimizações baseadas nos tipos conhecidos (embora o interpretador padrão não use isso, implementações alternativas podem).

Boas práticas e observações finais

Ao usar type checking, comece com um nível moderado de rigor e aumente conforme a confiança. Use Any com moderação, pois ele desativa a verificação. Prefira tipos específicos sempre que possível. Utilize Optional[T] para valores que podem ser None e Union[T1, T2] para múltiplos tipos.

Lembre-se de que o type checking não substitui testes. Ele complementa os testes, capturando uma classe de erros que os testes podem não cobrir. Integre o mypy ao seu pipeline de CI para garantir que todos os commits respeitem as anotações.

Exercícios

  1. Exercício 1: Instale o mypy e crie um script Python com uma função que soma dois números inteiros, mas sem anotações. Execute o mypy e observe que ele não reporta nada. Depois, adicione anotações de tipo e execute novamente. O que muda?
  2. ✓ Resposta: Sem anotações, o mypy não tem informação de tipo e não reporta nada. Com anotações, ele pode verificar se as chamadas são consistentes, mas se não houver chamadas incorretas, também não reporta nada. A diferença é que agora o código está documentado e o mypy pode detectar erros futuros.
  3. Exercício 2: Escreva uma função que recebe uma lista de inteiros e retorna a soma. Adicione anotações de tipo e use mypy para verificar. Depois, tente chamar a função com uma lista de strings e veja o erro.
  4. ✓ Resposta:
    from typing import List
    
    def soma_lista(numeros: List[int]) -> int:
        return sum(numeros)
    
    print(soma_lista([1, 2, 3]))
    # print(soma_lista(["a", "b"]))  # erro: Argument 1 to "soma_lista" has incompatible type "List[str]"; expected "List[int]"
  5. Exercício 3: Crie uma classe genérica Caixa que armazena um valor de qualquer tipo, com métodos set e get. Use TypeVar e Generic. Teste com diferentes tipos e use mypy para verificar.
  6. ✓ Resposta:
    from typing import Generic, TypeVar
    
    T = TypeVar("T")
    
    class Caixa(Generic[T]):
        def __init__(self) -> None:
            self.valor: T | None = None
    
        def set(self, novo_valor: T) -> None:
            self.valor = novo_valor
    
        def get(self) -> T | None:
            return self.valor
    
    caixa_int = Caixa[int]()
    caixa_int.set(10)
    print(caixa_int.get())
    
    caixa_str = Caixa[str]()
    caixa_str.set("Olá")
    print(caixa_str.get())
  7. Exercício 4: Explique o que são tipos graduais e como eles ajudam na migração de um código legado para a tipagem estática. Dê um exemplo de código onde você usa Any e depois substitui por um tipo mais específico.
  8. ✓ Resposta: Tipos graduais permitem adicionar anotações incrementalmente. No início, você pode usar Any para funções não tipadas e, aos poucos, substituir por tipos específicos. Por exemplo, def processar(item: Any) -> str pode virar def processar(item: str) -> str se você souber que sempre recebe strings.
  9. Exercício 5: Liste três benefícios do type checking e explique como ele pode melhorar a qualidade do código em um projeto real.
  10. ✓ Resposta: Benefícios: 1) Detecção precoce de erros, 2) Documentação embutida, 3) Facilita refatoração. Em um projeto real, isso reduz bugs, melhora a comunicação entre desenvolvedores e aumenta a confiança ao mudar o código.

Referências