Python

Artigo 44 — Qualidade de Código: linting, type hints e mypy Já leu

13 min de leitura

Artigo 44 — Qualidade de Código: linting, type hints e mypy
Testes verificam o que o código faz. Ferramentas de qualidade verificam como o código é escrito. Um código pode passar em todos os testes e ainda ser difícil

Artigo 44 — Qualidade de Código: linting, type hints e mypy

Prof. Ricardo Matos Módulo 8 · Testes, Qualidade e Boas Práticas · Artigo 44 de 52


Introdução

Testes verificam o que o código faz. Ferramentas de qualidade verificam como o código é escrito. Um código pode passar em todos os testes e ainda ser difícil de manter, inconsistente na formatação, cheio de variáveis não utilizadas ou propenso a bugs de tipo. Python tem um ecossistema rico de ferramentas que automatizam a verificação e formatação do código — tornando revisões de código mais produtivas e bases de código mais saudáveis.


Instalação

pip install ruff mypy black isort pylint bandit

PEP 8: O Guia de Estilo do Python

PEP 8 é o documento que define as convenções de estilo oficiais do Python. As principais regras:

# ✗ Errado
x=1
y = x+1
def calcular(a,b,c):
    return a+b+c
lista=[1,2,3,4,5]
if x==1 : print(x)

# ✓ Correto
x = 1
y = x + 1

def calcular(a, b, c):
    return a + b + c

lista = [1, 2, 3, 4, 5]

if x == 1:
    print(x)


# Nomeação
nome_variavel    = "snake_case"      # variáveis e funções
CONSTANTE_GLOBAL = 42               # constantes
class NomeClasse: ...                # PascalCase
_protegido       = "uso interno"    # prefixo _
__privado        = "name mangling"  # prefixo __

# Comprimento de linha: máximo 88 caracteres (black padrão)
# Dois espaços em branco entre funções e classes no topo do módulo
# Um espaço em branco entre métodos dentro de uma classe

Black: Formatação Automática

Black é um formatador opinativo — não há configuração de estilo, apenas um formato canônico:

# Verificar sem modificar
black --check src/

# Formatar todos os arquivos
black src/ tests/

# Formatar arquivo específico
black src/escola/aluno.py

# Ver diferença sem aplicar
black --diff src/escola/aluno.py
# Antes do Black
def calcular_media(notas,pesos=None,arredondar=True):
    if pesos==None:
        return sum(notas)/len(notas)
    else:
        return sum(n*p for n,p in zip(notas,pesos))/sum(pesos)

# Depois do Black
def calcular_media(notas, pesos=None, arredondar=True):
    if pesos == None:
        return sum(notas) / len(notas)
    else:
        return sum(n * p for n, p in zip(notas, pesos)) / sum(pesos)
# pyproject.toml
[tool.black]
line-length    = 88
target-version = ["py312"]
include        = '\.pyi?$'
exclude        = '''
/(
    \.git
  | \.venv
  | migrations
)/
'''

isort: Ordenação de Imports

isort src/ tests/
isort --check-only src/
isort --diff src/
# Antes do isort
import os
from typing import List
import numpy as np
from pathlib import Path
import sys
from escola.aluno import Aluno
import json
from datetime import datetime

# Depois do isort — agrupa: stdlib, third-party, local
import json
import os
import sys
from datetime import datetime
from pathlib import Path
from typing import List

import numpy as np

from escola.aluno import Aluno
# pyproject.toml
[tool.isort]
profile                   = "black"
line_length               = 88
known_first_party         = ["escola"]
known_third_party         = ["numpy", "pandas", "fastapi"]
multi_line_output         = 3
include_trailing_comma    = true
force_grid_wrap           = 0
use_parentheses           = true

Ruff: Linter Moderno e Ultrarrápido

Ruff substitui flake8, pylint e isort em um único binário escrito em Rust — 10-100x mais rápido:

# Verificar
ruff check src/

# Corrigir automaticamente
ruff check --fix src/

# Verificar e formatar
ruff check src/ && ruff format src/
# pyproject.toml
[tool.ruff]
line-length    = 88
target-version = "py312"

[tool.ruff.lint]
select = [
    "E",    # pycodestyle errors
    "W",    # pycodestyle warnings
    "F",    # pyflakes
    "I",    # isort
    "B",    # flake8-bugbear
    "C4",   # flake8-comprehensions
    "UP",   # pyupgrade
    "N",    # pep8-naming
    "SIM",  # flake8-simplify
    "TCH",  # flake8-type-checking
    "ANN",  # flake8-annotations
    "S",    # flake8-bandit (segurança)
    "PTH",  # flake8-use-pathlib
    "RUF",  # ruff-specific rules
]
ignore = [
    "ANN101",  # missing type annotation for self
    "ANN102",  # missing type annotation for cls
    "S101",    # use of assert detected (ok em testes)
]

[tool.ruff.lint.per-file-ignores]
"tests/**" = ["ANN", "S101"]
"migrations/**" = ["E501"]

O que o Ruff detecta

# F401 — import não utilizado
import os        # nunca usado

# F841 — variável não utilizada
def func():
    resultado = calcular()   # nunca usada
    return 42

# E711 — comparação com None errada
if x == None:    # ✗
if x is None:    # ✓

# B006 — argumento mutável padrão
def func(lista=[]):    # ✗ — perigoso!
    lista.append(1)

def func(lista=None):  # ✓
    if lista is None:
        lista = []

# UP006 — use list ao invés de List (Python 3.9+)
from typing import List
def func(x: List[int]) -> List[str]:  # ✗
def func(x: list[int]) -> list[str]:  # ✓

# SIM108 — use operador ternário
if condicao:   # ✗
    x = 1
else:
    x = 2
x = 1 if condicao else 2  # ✓

# PTH123 — use pathlib ao invés de open
f = open("arquivo.txt")          # ✗
f = Path("arquivo.txt").open()   # ✓

Type Hints: Anotações de Tipo

Type hints tornaram-se essenciais no Python moderno — documentam intenção, previnem bugs e habilitam ferramentas de análise estática:

# Básico
def somar(a: int, b: int) -> int:
    return a + b

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

# Coleções (Python 3.9+)
def processar(numeros: list[int]) -> dict[str, float]:
    return {"media": sum(numeros) / len(numeros)}

# Optional — valor pode ser None
from typing import Optional

def buscar(id: int) -> Optional[str]:
    return None

# Equivalente moderno (Python 3.10+)
def buscar(id: int) -> str | None:
    return None

# Union — múltiplos tipos possíveis
def converter(valor: int | float | str) -> float:
    return float(valor)

# Callable — funções como tipo
from typing import Callable

def aplicar(func: Callable[[int], int], valor: int) -> int:
    return func(valor)

# TypeVar — genéricos
from typing import TypeVar, Generic

T = TypeVar("T")

def primeiro(lista: list[T]) -> T | None:
    return lista[0] if lista else None

# Literal — valores específicos
from typing import Literal

Ambiente = Literal["desenvolvimento", "producao", "testes"]

def configurar(env: Ambiente) -> None:
    pass

# TypedDict — dicionários com tipos fixos
from typing import TypedDict

class ConfigDB(TypedDict):
    host:  str
    porta: int
    banco: str
    usuario: str

def conectar(config: ConfigDB) -> None:
    pass

# Protocol — duck typing com tipos
from typing import Protocol

class Serializavel(Protocol):
    def serializar(self) -> str: ...
    def deserializar(self, dados: str) -> None: ...

# dataclass com tipos
from dataclasses import dataclass

@dataclass
class Ponto:
    x: float
    y: float
    label: str = ""

    def distancia(self, outro: "Ponto") -> float:
        return ((self.x - outro.x) ** 2 + (self.y - outro.y) ** 2) ** 0.5

Tipos Avançados

from typing import (
    Any, ClassVar, Final,
    Generator, Iterator, AsyncIterator,
    overload, TYPE_CHECKING
)
from collections.abc import Sequence, Mapping

# Final — constantes imutáveis
MAX_TENTATIVAS: Final = 3
VERSAO: Final[str]    = "1.0.0"

# ClassVar — atributos de classe
class Contador:
    _instancias: ClassVar[int] = 0

    def __init__(self):
        Contador._instancias += 1

# Generator
def fibonacci() -> Generator[int, None, None]:
    a, b = 0, 1
    while True:
        yield a
        a, b = b, a + b

# overload — múltiplas assinaturas
@overload
def processar(valor: int) -> int: ...
@overload
def processar(valor: str) -> str: ...

def processar(valor):
    if isinstance(valor, int):
        return valor * 2
    return valor.upper()

# TYPE_CHECKING — imports apenas para type checking
if TYPE_CHECKING:
    from escola.turma import Turma

class Aluno:
    def get_turma(self) -> "Turma":  # evita import circular
        ...

# ParamSpec — para decoradores que preservam assinatura
from typing import ParamSpec, TypeVar

P = ParamSpec("P")
R = TypeVar("R")

def log_chamada(func: Callable[P, R]) -> Callable[P, R]:
    def wrapper(*args: P.args, **kwargs: P.kwargs) -> R:
        print(f"Chamando {func.__name__}")
        return func(*args, **kwargs)
    return wrapper

@log_chamada
def calcular(a: int, b: int) -> int:
    return a + b

mypy: Verificação Estática de Tipos

# Verificação básica
mypy src/

# Com configuração estrita
mypy --strict src/

# Arquivo específico
mypy src/escola/aluno.py
# pyproject.toml
[tool.mypy]
python_version          = "3.12"
strict                  = true
warn_return_any         = true
warn_unused_configs     = true
warn_redundant_casts    = true
warn_unused_ignores     = true
disallow_untyped_defs   = true
disallow_incomplete_defs = true
check_untyped_defs      = true
no_implicit_optional    = true
show_error_codes        = true
pretty                  = true

[[tool.mypy.overrides]]
module = ["numpy.*", "pandas.*", "sklearn.*"]
ignore_missing_imports  = true
# Erros comuns detectados pelo mypy

# Tipo incompatível
def somar(a: int, b: int) -> int:
    return a + b

somar(1, "dois")   # error: Argument 2 has incompatible type "str"; expected "int"

# Retorno errado
def get_nome() -> str:
    return None    # error: Incompatible return value type (got "None", expected "str")

# Atributo possivelmente None
def processar(aluno: Aluno | None) -> str:
    return aluno.nome    # error: Item "None" of "Aluno | None" has no attribute "nome"

# Correto:
def processar(aluno: Aluno | None) -> str:
    if aluno is None:
        return "Desconhecido"
    return aluno.nome

# Operação em tipo errado
valores: list[int] = [1, 2, 3]
valores.append("quatro")   # error: Argument 1 has incompatible type "str"

# Ignorando erro específico quando necessário
resultado = funcao_legada()  # type: ignore[no-untyped-call]

Bandit: Segurança no Código

pip install bandit
bandit -r src/
bandit -r src/ -f json -o relatorio_seguranca.json
# Vulnerabilidades que o Bandit detecta

# B105 — senha hardcoded
senha = "admin123"          # ✗ Bandit avisa

# B108 — diretório temp inseguro
import tempfile
tempfile.mktemp()           # ✗ use mkstemp() ou TemporaryFile()

# B301 — pickle inseguro
import pickle
dados = pickle.loads(entrada_usuario)  # ✗ pode executar código arbitrário

# B501 — SSL sem verificação
import requests
requests.get(url, verify=False)  # ✗

# B506 — yaml.load sem Loader
import yaml
yaml.load(conteudo)              # ✗
yaml.safe_load(conteudo)         # ✓

# B608 — SQL injection potencial
query = f"SELECT * FROM alunos WHERE id = {id}"  # ✗
cursor.execute("SELECT * FROM alunos WHERE id = ?", (id,))  # ✓

pre-commit: Automação antes do Commit

pip install pre-commit
# .pre-commit-config.yaml
repos:
  - repo: https://github.com/pre-commit/pre-commit-hooks
    rev: v4.5.0
    hooks:
      - id: trailing-whitespace
      - id: end-of-file-fixer
      - id: check-yaml
      - id: check-json
      - id: check-toml
      - id: check-merge-conflict
      - id: debug-statements      # detecta print() e breakpoint()
      - id: detect-private-key    # detecta chaves privadas

  - repo: https://github.com/astral-sh/ruff-pre-commit
    rev: v0.3.0
    hooks:
      - id: ruff
        args: [--fix]
      - id: ruff-format

  - repo: https://github.com/psf/black
    rev: 24.3.0
    hooks:
      - id: black

  - repo: https://github.com/pre-commit/mirrors-mypy
    rev: v1.9.0
    hooks:
      - id: mypy
        additional_dependencies: [types-requests]

  - repo: https://github.com/PyCQA/bandit
    rev: 1.7.8
    hooks:
      - id: bandit
        args: ["-c", "pyproject.toml"]
# Instalando os hooks
pre-commit install

# Rodando manualmente em todos os arquivos
pre-commit run --all-files

# Rodando hook específico
pre-commit run black --all-files

# Atualizando versões dos hooks
pre-commit autoupdate

Exemplo Completo: Código Bem Tipado

# src/escola/repositorio.py
from __future__ import annotations

import json
from pathlib import Path
from typing import ClassVar, Final, TypeVar

from escola.aluno import Aluno
from escola.exceptions import AlunoNaoEncontradoError, EmailDuplicadoError

T = TypeVar("T", bound="BaseRepositorio")

VERSAO_SCHEMA: Final[str] = "1.0"


class BaseRepositorio:
    """Classe base para repositórios."""

    _instancia: ClassVar[BaseRepositorio | None] = None

    def __init_subclass__(cls, **kwargs: object) -> None:
        super().__init_subclass__(**kwargs)


class RepositorioAlunos(BaseRepositorio):
    """Repositório de alunos com persistência em JSON."""

    def __init__(self, arquivo: Path | None = None) -> None:
        self._dados:     dict[int, Aluno] = {}
        self._proximo_id: int = 1
        self._arquivo = arquivo

        if arquivo and arquivo.exists():
            self._carregar(arquivo)

    def salvar(self, aluno: Aluno) -> int:
        """Salva um aluno e retorna seu ID."""
        email_existente = self.buscar_por_email(aluno.email)
        if email_existente:
            raise EmailDuplicadoError(aluno.email)

        aluno_id = self._proximo_id
        self._dados[aluno_id] = aluno
        self._proximo_id += 1

        if self._arquivo:
            self._persistir()

        return aluno_id

    def buscar(self, aluno_id: int) -> Aluno:
        """Busca aluno por ID — levanta erro se não encontrado."""
        aluno = self._dados.get(aluno_id)
        if aluno is None:
            raise AlunoNaoEncontradoError(aluno_id)
        return aluno

    def buscar_por_email(self, email: str) -> Aluno | None:
        """Retorna None se não encontrado."""
        return next(
            (a for a in self._dados.values() if a.email == email),
            None
        )

    def listar(
        self,
        turma: str | None = None,
        apenas_ativos: bool = True,
    ) -> list[Aluno]:
        """Lista alunos com filtros opcionais."""
        resultado: list[Aluno] = list(self._dados.values())

        if apenas_ativos:
            resultado = [a for a in resultado if a.ativo]
        if turma:
            resultado = [a for a in resultado if a.turma == turma]

        return sorted(resultado, key=lambda a: a.nome)

    def deletar(self, aluno_id: int) -> None:
        """Remove aluno — levanta erro se não encontrado."""
        self.buscar(aluno_id)   # garante que existe
        del self._dados[aluno_id]

    def _persistir(self) -> None:
        """Persiste dados em arquivo JSON."""
        if self._arquivo is None:
            return
        dados_json = {
            "versao": VERSAO_SCHEMA,
            "alunos": {
                str(k): {
                    "nome":  v.nome,
                    "email": v.email,
                    "turma": v.turma,
                    "notas": v.notas,
                    "ativo": v.ativo,
                }
                for k, v in self._dados.items()
            }
        }
        self._arquivo.write_text(
            json.dumps(dados_json, indent=2, ensure_ascii=False),
            encoding="utf-8"
        )

    def _carregar(self, arquivo: Path) -> None:
        """Carrega dados de arquivo JSON."""
        conteudo = json.loads(arquivo.read_text(encoding="utf-8"))
        for id_str, dados in conteudo.get("alunos", {}).items():
            aluno = Aluno(
                nome=  dados["nome"],
                email= dados["email"],
                turma= dados["turma"],
                ativo= dados.get("ativo", True),
            )
            aluno.notas = dados.get("notas", [])
            self._dados[int(id_str)] = aluno
            self._proximo_id = max(self._proximo_id, int(id_str) + 1)
# src/escola/exceptions.py
class EscolaBaseError(Exception):
    """Exceção base do domínio escola."""


class AlunoNaoEncontradoError(EscolaBaseError):
    def __init__(self, aluno_id: int) -> None:
        self.aluno_id = aluno_id
        super().__init__(f"Aluno com id={aluno_id} não encontrado.")


class EmailDuplicadoError(EscolaBaseError):
    def __init__(self, email: str) -> None:
        self.email = email
        super().__init__(f"E-mail já cadastrado: {email}")


class NotaInvalidaError(EscolaBaseError):
    def __init__(self, nota: float) -> None:
        self.nota = nota
        super().__init__(f"Nota inválida: {nota}. Deve estar entre 0 e 10.")

Resumo

  • PEP 8 define as convenções de estilo — Black e Ruff as aplicam automaticamente
  • Black formata sem configuração de estilo — elimina debates sobre formatação em equipes
  • Ruff substitui flake8 + isort + pylint em um único binário 100x mais rápido
  • Type hints documentam a intenção do código e habilitam análise estática com mypy
  • mypy detecta erros de tipo antes da execução — especialmente valioso em bases grandes
  • Bandit analisa o código em busca de vulnerabilidades de segurança comuns
  • pre-commit automatiza todas as verificações antes de cada commit — zero esforço manual
  • Código bem tipado com exceções específicas de domínio é autoexplicativo e fácil de testar

Referências e Leituras Complementares

  • PEP 8 — guia de estilo — https://peps.python.org/pep-0008/
  • Black — documentação — https://black.readthedocs.io/en/stable/
  • Ruff — documentação — https://docs.astral.sh/ruff/
  • mypy — documentação — https://mypy.readthedocs.io/en/stable/
  • Bandit — documentação — https://bandit.readthedocs.io/en/latest/
  • pre-commit — documentação — https://pre-commit.com/
  • PEP 484 — Type Hints — https://peps.python.org/pep-0484/
  • PEP 526 — Variable Annotations — https://peps.python.org/pep-0526/
  • PERCIVAL, Harry; GREGORY, Bob. Architecture Patterns with Python. O'Reilly Media, 2020. — código limpo, tipado e testável em projetos Python profissionais.

Prof. Ricardo Matos — Dominando o Python em 1 Ano · Artigo 44 de 52 Próximo: Artigo 45 — CI/CD com GitHub Actions


you asked

Sim


claude response

Dominando o Python em 1 Ano

Comentários

Mais em Python

Concorrência: threads, processos e async/await
Concorrência: threads, processos e async/await

Todo programa que vimos até aqui executa uma instrução por vez — modelo seque...

Trabalhando com Datas e Horas
Trabalhando com Datas e Horas

Datas e horas estão em todo lugar no desenvolvimento de software — registros...

Algoritmos de Ordenação e Busca
Algoritmos de Ordenação e Busca

Ordenar e buscar dados são duas das operações mais funda...