Inteligência artificial, sem ruído.
Tutoriais7 min

De código espaguete a Python limpo: um guia de refatoração passo a passo

Aprenda a separar responsabilidades, usar dataclasses e testar cada função — e elimine bugs causados por ordem de execução.

De código espaguete a Python limpo: um guia de refatoração passo a passo

Se você já abriu um script antigo e teve medo de mexer nele, você sabe o que é código espaguete: lógica emaranhada, dependências invisíveis e a sensação de que qualquer mudança pode quebrar algo do outro lado do arquivo. Com a IA generativa acelerando a escrita de código, essa dívida técnica tende a crescer ainda mais rápido — e refatorar bem virou uma habilidade essencial, não um luxo.

Neste guia, você vai ver, passo a passo, como transformar uma função Python bagunçada em código limpo e testável, usando conceitos que valem para qualquer projeto — do script de análise de dados ao serviço em produção.

✅ O que você ganha

  • Funções com responsabilidade única, fáceis de ler e de testar
  • Menos bugs causados por efeitos colaterais e ordem de execução
  • Testes que apontam exatamente onde está o problema
  • Tipagem explícita que deixa o linter pegar erros antes da execução
  • Um padrão replicável para qualquer código que “cresceu demais”

⚠️ O que você NÃO ganha

  • Refatorar não corrige lógica de negócio errada — só a torna mais visível
  • Não é uma bala de prata: exige disciplina e aumenta o número de funções no arquivo
  • Dataclasses e tipagem adicionam um pouco de verbosidade em scripts muito pequenos

Requisitos

ComponenteMínimoRecomendado
Python3.83.11+
EditorQualquer umVS Code + extensão Python
Testesassert simplespytest
Conhecimento prévioFunções e dicionáriosDataclasses e type hints
Tempo estimado30–45 minutos
Requisitos para acompanhar a refatoração

O ponto de partida: uma função que faz tudo

Considere uma função que processa pedidos de uma loja online. Ela calcula desconto, atualiza estoque e “envia” e-mail — tudo no mesmo loop:

inventory = {"sku-1042": 18, "sku-2077": 4}

def process_order(order):
    total = 0
    for item in order["items"]:
        price = item["unit_price"] * item["quantity"]
        if order["customer_type"] == "vip":
            price = price * 0.85
        elif order["customer_type"] == "regular" and total > 100:
            price = price * 0.95
        total += price
        if item["sku"] in inventory:
            inventory[item["sku"]] -= item["quantity"]
        else:
            print(f"Warning: {item['sku']} not found in inventory")
    if total > 500:
        shipping = 0
    else:
        shipping = 12.99
    total += shipping
    print(f"Sending confirmation email to {order['customer_email']}")
    print(f"Order total: ${total:.2f}")
    return total

Há um bug escondido aí: o desconto de cliente comum verifica total > 100no meio do loop, quando o total ainda está incompleto. O resultado depende da ordem em que os itens aparecem — um erro clássico de “lógica emaranhada”.

Os sinais de alerta para identificar código espaguete no seu projeto: uma função cujo nome não descreve tudo o que ela faz, uma variável que muda de significado ao longo da função, e qualquer cálculo que dependa da ordem em que as instruções executam.

Passo 1 — Separe cada responsabilidade

Dê a cada tarefa a sua própria função, com entrada e retorno claros:

def calculate_subtotal(items):
    return sum(item.unit_price * item.quantity for item in items)

def apply_discount(subtotal, customer_type):
    if customer_type == "vip":
        return subtotal * 0.85
    if customer_type == "regular" and subtotal > 100:
        return subtotal * 0.95
    return subtotal

def calculate_shipping(discounted_total):
    return 0.0 if discounted_total > 500 else 12.99

Agora apply_discount trabalha com o subtotal finalizado, não com um total em andamento — o bug de ordem de execução desaparece como consequência direta da separação. Cada função pode ser chamada isoladamente e você sabe exatamente o que ela faz.

Passo 2 — Troque dicionários por dataclasses

Passar dicionários com chaves de string funciona, mas não garante quais campos existem nem seus tipos. Uma dataclass dá estrutura explícita:

from dataclasses import dataclass

@dataclass
class OrderItem:
    sku: str
    unit_price: float
    quantity: int

@dataclass
class Order:
    customer_email: str
    customer_type: str
    items: list[OrderItem]

Com o formato definido, o restante do código escreve contra uma forma conhecida, em vez de adivinhar chaves de dicionário. O linter consegue pegar, por exemplo, uma chamada que passa um dict onde deveria vir um Order.

Passo 3 — Transforme a função em coordenadora

def process_order(order: Order, inventory: dict) -> float:
    subtotal = calculate_subtotal(order.items)
    discounted = apply_discount(subtotal, order.customer_type)
    total = discounted + calculate_shipping(discounted)
    update_inventory(order.items, inventory)
    return total

process_order agora é uma coordenadora, não uma trabalhadora. Ler de cima para baixo conta a história completa: calcular, aplicar desconto, calcular frete, atualizar estoque.

Passo 4 — Levante erros em vez de imprimir avisos

A função original imprimia um aviso quando um SKU não existia e seguia em frente — ou seja, um item faltando nunca interrompia nada. A versão correta falha explicitamente no ponto em que ocorre:

def update_inventory(items, inventory):
    for item in items:
        if item.sku not in inventory:
            raise ValueError(f"{item.sku} not found in inventory")
        inventory[item.sku] -= item.quantity

Lançar uma exceção torna a falha explícita e impede que o pedido continue quando a atualização de estoque não foi concluída. Também facilita detectar o problema em testes e rastrear na hora de depurar.

Passo 5 — Teste cada peça isoladamente

def test_apply_discount_vip():
    assert apply_discount(200, "vip") == 170.0

def test_apply_discount_regular_under_threshold():
    assert apply_discount(80, "regular") == 80

Com o pytest, um teste que falha aponta direto para a regra de desconto — em vez de um relatório vago de “o total do pedido veio errado” sem indicar qual das quatro responsabilidades falhou.

Casos de uso reais

  • ETL de dados: separar extração, transformação e carga em funções puras facilita testar cada etapa sem rodar o pipeline inteiro
  • APIs: validar entrada em uma função dedicada e devolver erros estruturados evita validação espalhada pelo handler
  • Scripts de CI/CD: funções pequenas e testáveis tornam o pipeline de deploy menos frágil a mudanças
  • Código legado: aplicar o padrão uma função por vez mantém o script funcionando durante toda a refatoração

Troubleshooting

  • ❌ TypeError ao acessar chave de dicionário → Causa: ainda passando dict onde deveria usar a dataclass. Solução: substitua o acesso por atributos (order.customer_type) e atualize as chamadas.
  • ❌ Teste falha com valor inesperado de desconto → Causa: cálculo dependendo de estado parcial. Solução: garanta que o desconto receba o subtotal final, não um total em andamento.
  • ❌ ValueError de SKU não encontrado em produção → Causa: erro agora é explícito, em vez de só um print. Solução: trate a exceção no chamador ou valide o estoque antes do loop.
  • ❌ Linter reclama de tipo incorreto → Causa: type hints adicionados. Solução: alinhe as assinaturas das funções com as dataclasses.
  • ❌ Import circular ao extrair funções → Causa: módulos com dependência mútua. Solução: mova as funções puras para um módulo compartilhado sem dependências.

FAQ

  • Preciso refatorar o código inteiro de uma vez? Não. O padrão recomendado é uma função por vez, mantendo o script funcionando a cada passo e facilitando a revisão.
  • Dataclasses são obrigatórias? Não, mas ajudam quando os dados têm estrutura conhecida. Para scripts triviais, tuplas nomeadas ou dicionários simples bastam.
  • Quando vale a pena usar pytest em vez de assert? Quando você quer relatórios claros, parametrização de testes e integração com CI. Para um teste rápido, assert simples resolve.
  • Refatorar pode introduzir bugs? Sim, se feita sem testes. Extraia uma função, escreva o teste correspondente e só então avance para a próxima.
  • Isso se aplica a código de IA? Sim — pipelines de ML, pré-processamento e agentes sofrem dos mesmos problemas de acoplamento e são candidatos naturais à refatoração.

O caminho à frente

Com ferramentas de IA escrevendo cada vez mais código, a capacidade de ler e refatorar se tornou mais valiosa que a de escrever do zero. O padrão apresentado aqui — separar responsabilidades, modelar dados explicitamente e testar cada peça — tende a ser a linha de frente da manutenção de software na era dos agentes de código. Quem domina isso consegue manter sistemas legíveis e seguros mesmo quando o volume de código cresce rápido.


Descubra mais sobre noticiAI

Assine para receber nossas notícias mais recentes por e-mail.

R
Sobre o autorRedação Noticiai

Equipe editorial dedicada a explicar inteligência artificial com clareza, independência e contexto.