Feature Flags em Python: deploy seguro sem medo de produção

Aprenda a usar feature flags em aplicações Python para lançar funcionalidades aos poucos, fazer rollback rápido, testar com grupos pequenos e reduzir risco em produção.

8 min de leitura Equipe Python Brasil

Deploy seguro não depende só de escrever bons testes. Em aplicações Python reais, uma mudança pode passar pela suíte automatizada, funcionar no staging e ainda falhar quando encontra tráfego, dados antigos, integrações instáveis ou usuários com comportamento inesperado. Feature flags reduzem esse risco porque separam o ato de publicar código do ato de liberar uma funcionalidade para todo mundo.

Uma feature flag é uma condição controlada por configuração, banco, Redis, serviço externo ou variável de ambiente. O código novo já está no deploy, mas só roda quando a flag permite. Isso cria um caminho de rollback rápido: em vez de reverter commit, reconstruir imagem e aguardar outro deploy, o time desliga a flag e volta ao comportamento antigo.

Este guia mostra como pensar feature flags em Python sem exagerar na arquitetura. Ele complementa conteúdos como FastAPI Background Tasks, Celery e Redis, Python e Redis, logging em Python e testes unitários com Python. O objetivo é entregar uma base prática para APIs, jobs, automações e produtos internos.

Quando usar feature flags

Feature flags fazem sentido quando uma mudança precisa ser liberada com controle gradual. Exemplos comuns:

  • trocar um endpoint antigo por uma implementação nova;
  • ativar um novo fluxo de checkout para poucos usuários;
  • testar um modelo de recomendação com uma porcentagem pequena do tráfego;
  • liberar uma tela interna apenas para o time de suporte;
  • proteger uma integração externa que pode ficar instável;
  • migrar de uma fila simples para Celery sem mudar tudo de uma vez;
  • desligar uma rotina cara durante incidente de performance.

Elas não substituem testes, revisão de código, observabilidade ou deploy bem feito. A flag é uma camada adicional de controle. Se a mudança é pequena, reversível e sem impacto operacional, talvez um deploy normal seja suficiente. Se a mudança altera comportamento crítico, dados de cliente, cobrança, autenticação, performance ou integração com terceiros, uma flag costuma valer o custo.

Comece simples com variável de ambiente

O jeito mais simples de implementar uma flag é ler uma variável de ambiente. Isso funciona bem para flags globais, usadas por toda a aplicação.

import os


def flag_ativa(nome: str, padrao: bool = False) -> bool:
    valor = os.getenv(nome)
    if valor is None:
        return padrao
    return valor.strip().lower() in {"1", "true", "yes", "on"}


USAR_NOVO_CALCULO_FRETE = flag_ativa("USAR_NOVO_CALCULO_FRETE")


def calcular_frete(pedido):
    if USAR_NOVO_CALCULO_FRETE:
        return calcular_frete_v2(pedido)
    return calcular_frete_v1(pedido)

Esse padrão é fácil de entender, testar e operar em ambientes pequenos. O ponto fraco é que mudar a flag geralmente exige reiniciar o processo ou fazer novo deploy, dependendo da plataforma. Para uma flag de segurança operacional, isso pode ser lento demais. Para uma flag de migração planejada, pode ser suficiente.

Uma regra prática: use variável de ambiente para flags de infraestrutura e comportamento global, não para experimentos por usuário.

Flags por usuário ou porcentagem

Quando você quer liberar uma funcionalidade para 5%, 20% ou 50% dos usuários, precisa de uma decisão estável. O mesmo usuário deve cair no mesmo grupo em requisições diferentes, senão a experiência fica confusa.

Um caminho simples é usar hash determinístico:

from hashlib import sha256


def em_rollout(usuario_id: str, nome_flag: str, percentual: int) -> bool:
    if percentual <= 0:
        return False
    if percentual >= 100:
        return True

    chave = f"{nome_flag}:{usuario_id}".encode("utf-8")
    bucket = int(sha256(chave).hexdigest(), 16) % 100
    return bucket < percentual


def deve_usar_recomendacao_nova(usuario_id: str) -> bool:
    return em_rollout(usuario_id, "recomendacao_v2", percentual=10)

Esse exemplo não depende de serviço externo e mantém a decisão estável. Se o percentual subir de 10 para 25, os usuários dos primeiros 10% continuam incluídos e mais usuários entram. Isso facilita rollout gradual.

Para produtos com regras complexas, vale usar banco, painel interno ou ferramenta especializada. Mas a lógica de base é a mesma: uma flag deve responder de forma previsível, auditável e rápida.

Exemplo com FastAPI

Em uma API FastAPI, a flag pode decidir qual implementação atende uma rota. O exemplo abaixo usa uma função simples, mas em produção a decisão poderia vir de Redis, banco ou configuração carregada em memória.

from fastapi import FastAPI, Header

app = FastAPI()


def usuario_em_beta(user_id: str | None) -> bool:
    if not user_id:
        return False
    return em_rollout(user_id, "busca_nova", percentual=15)


@app.get("/busca")
def buscar(q: str, x_user_id: str | None = Header(default=None)):
    if usuario_em_beta(x_user_id):
        return buscar_com_motor_novo(q)
    return buscar_com_motor_antigo(q)

Esse formato permite validar comportamento real com um grupo pequeno antes de liberar para todos. Se métricas piorarem, reduza o percentual para zero. Se tudo estiver saudável, aumente aos poucos.

Em endpoints críticos, registre nos logs qual variação foi usada. Sem isso, fica difícil investigar se um erro veio do código novo ou do antigo.

import logging

logger = logging.getLogger(__name__)


def registrar_flag(nome: str, ativa: bool, usuario_id: str | None):
    logger.info(
        "feature_flag_decidida",
        extra={"flag": nome, "ativa": ativa, "usuario_id": usuario_id},
    )

Se sua stack coleta logs estruturados, inclua flag, variant, request_id e user_id quando for permitido pela política de privacidade. Isso ajuda a cruzar erros, latência e conversão.

Redis como armazenamento operacional

Redis é útil quando a flag precisa mudar sem redeploy. A aplicação consulta uma chave com TTL ou cache local curto.

import redis

r = redis.Redis.from_url("redis://localhost:6379/0", decode_responses=True)


def flag_redis(nome: str, padrao: bool = False) -> bool:
    valor = r.get(f"feature:{nome}")
    if valor is None:
        return padrao
    return valor.lower() in {"1", "true", "on"}

O cuidado principal é disponibilidade. Se Redis cair, a aplicação deve ter um padrão seguro. Para flags de ativação de funcionalidade nova, o padrão normalmente é False. Para flags de proteção, como “desligar processamento pesado”, talvez o padrão seguro seja True, dependendo do desenho.

Evite consultar Redis em toda função interna sem cache ou limite. Em alto tráfego, carregue flags em memória por alguns segundos ou use uma camada de configuração atualizada periodicamente. O objetivo é reduzir risco, não criar uma dependência lenta no caminho quente de cada requisição.

Boas práticas para não virar bagunça

Feature flags envelhecem mal quando ninguém remove código antigo. Cada flag deve ter dono, data de criação, objetivo e critério de remoção. Sem isso, o projeto acumula if permanentes que tornam o código difícil de testar.

Boas práticas simples:

  • dê nomes específicos, como checkout_pix_v2, não nova_funcionalidade;
  • documente o que acontece quando a flag está ligada e desligada;
  • escreva testes para os dois caminhos enquanto a flag existir;
  • registre decisões importantes em logs ou métricas;
  • remova a flag depois que a mudança estiver 100% liberada e estável;
  • evite flags aninhadas dentro de flags;
  • não use feature flag como substituta para permissão de acesso sensível.

Também existe diferença entre flag de release, flag operacional e flag de experimento. Flag de release controla quando uma funcionalidade aparece. Flag operacional desliga ou reduz uma parte do sistema em incidente. Flag de experimento compara variações de produto. Misturar tudo no mesmo nome e sem metadados gera confusão.

Como testar código com flags

Teste os dois caminhos explicitamente. Em pytest, prefira injetar a decisão da flag ou usar monkeypatch em vez de depender do ambiente real.

def calcular_total(carrinho, usar_desconto_novo: bool):
    if usar_desconto_novo:
        return calcular_total_v2(carrinho)
    return calcular_total_v1(carrinho)


def test_total_com_regra_antiga(carrinho):
    assert calcular_total(carrinho, usar_desconto_novo=False) == 100


def test_total_com_regra_nova(carrinho):
    assert calcular_total(carrinho, usar_desconto_novo=True) == 90

Essa separação deixa a regra mais fácil de validar. O código que lê ambiente, Redis ou banco deve ser pequeno; a lógica de negócio deve receber uma decisão simples.

Em testes end-to-end, cubra pelo menos o caminho principal com a flag ligada antes do rollout. Se a mudança afeta interface, combine a flag com testes de navegador, como no guia de Playwright com Python.

Checklist de rollout

Antes de ligar uma flag em produção, responda:

PerguntaPor que importa
Qual métrica deve melhorar ou permanecer estável?Evita rollout baseado em sensação
Como desligar rapidamente?Define rollback operacional
O caminho antigo ainda funciona?Garante recuperação
A flag aparece nos logs?Facilita diagnóstico
Existe alerta de erro ou latência?Detecta regressão cedo
Quem é o dono da flag?Evita abandono
Quando remover?Reduz dívida técnica

Um rollout saudável começa pequeno: equipe interna, 1%, 5%, 25%, 50% e 100%, com pausas para observar métricas. Nem todo projeto precisa desses passos formais, mas mudanças críticas se beneficiam desse ritmo.

Relação com outras linguagens

O conceito não é exclusivo de Python. Times que rodam serviços de alta concorrência em Go usam flags para controlar rollout de APIs, workers e migrações. Projetos que combinam Python com extensões de performance em Rust também podem liberar gradualmente o caminho otimizado, mantendo fallback em Python enquanto validam resultados.

Para o mercado, isso é uma habilidade valiosa. Saber implementar flags mostra maturidade de produção: você não pensa apenas em “subir código”, mas em controlar risco, medir impacto e recuperar rápido quando algo sai errado.

Conclusão

Feature flags são uma das técnicas mais práticas para tornar deploys Python menos assustadores. Comece com variável de ambiente quando a necessidade for simples. Use hash determinístico para rollout por usuário. Considere Redis ou banco quando a operação precisa mudar flags sem redeploy. Em todos os casos, mantenha testes, logs, dono e plano de remoção.

O melhor sinal de uma boa feature flag é que ela desaparece depois de cumprir seu papel. Ela protege o rollout, acelera a aprendizagem e depois sai do caminho para o código voltar a ficar simples.

E

Equipe Python Brasil

Contribuidor do Python Brasil — Aprenda Python em Português