Riverfount

Um espaço para meus devaneios, sejam em TI ou em Filosofia

Saber dar bons nomes é uma das habilidades mais valiosas — e menos ensinadas — na engenharia de software. Em Python, nomes de variáveis e funções bem escolhidos tornam o código legível, reduzem ambiguidade e ajudam a preservar o design ao longo do tempo. Seguindo as diretrizes da PEP 8 e os princípios da Clean Architecture, este artigo mostra como criar nomes expressivos, consistentes e concisos, sem cair na armadilha dos identificadores longos ou genéricos. Você verá exemplos reais, más práticas comuns e um mini refactor que demonstra como nomes claros transformam o código.

1. Nomes são parte do design

Um código pode estar correto e, ainda assim, ser difícil de entender. Na maioria das vezes, o problema está nos nomes. Na Clean Architecture, os nomes devem refletir conceitos de negócio, não detalhes técnicos ou estruturais.

Má prática Boa prática Por quê?
db_user user_account Remove o detalhe técnico e foca no domínio.
json_response order_summary “JSON” é formato, não conceito.
user_data customer_profile “Data” é genérico; “profile” tem significado.

A lógica é simples: nomeie pelo propósito, não pela forma.

2. PEP 8: legibilidade é prioridade

A PEP 8 vai muito além da estética — ela é um guia de comunicação entre pessoas.
Algumas regras práticas:

  • Use snake_case para variáveis e funções.
  • Evite abreviações desnecessárias (cfg, cnt, ttl). Prefira nomes completos (config, count, total).
  • Use plural para coleções (users, orders) e singular para elementos únicos (user, order).
  • Remova redundâncias no contexto: dentro de UserService, prefira get_user() a get_user_data().
# ruim
def list_all_active_user_objects():
    ...

# bom
def list_active_users():
    ...

No segundo exemplo, o nome é simples e direto — o leitor entende a intenção de imediato.

3. Contexto é autoexplicativo

Bons nomes reduzem a necessidade de comentários. O código deve ser quase uma frase legível.

# ruim
data = get_data()

# bom
user_orders = order_service.fetch_recent_orders(user_id)

Outro exemplo comum:

# ruim
flag = True
if flag:
    process()

# bom
should_notify = True
if should_notify:
    send_notification()

Quando as variáveis comunicam intenção, o raciocínio flui naturalmente — o código se torna autoexplicativo.

4. Clareza e concisão

Nomes longos demais são tão ruins quanto nomes curtos e vagos.
O segredo é deixar o contexto carregar parte do significado.

Má prática Boa prática Justificativa
customer_account_balance_after_transaction_update new_balance O contexto já comunica o momento.
temporary_order_price_value temp_price Clareza mantida, sem prolixidade.
is_user_valid_and_authenticated is_authenticated Detalhes extras só atrapalham.

A clareza vem do contexto, não do tamanho do nome.

5. Nomear é projetar

Nomes são uma peça invisível da arquitetura do sistema. Quando todas as partes falam a mesma língua — a do negócio —, o código mantém coesão e resiliência. Trocar o banco ou o framework é fácil; perder clareza semântica, porém, é caro.

Bons nomes preservam a intenção arquitetural — mesmo após refatorações.
Eles são a ponte entre design técnico e linguagem de domínio.

6. Checklist rápido de boas práticas

  1. Use a linguagem do domínio, não da tecnologia.
  2. Seja claro, mas evite redundâncias.
  3. Adapte a granularidade: nomes locais curtos, nomes globais descritivos.
  4. Descreva propósito, não formato técnico.
  5. Evite genéricos (data, info, object).
  6. Mantenha consistência terminológica.
  7. Não exponha infraestrutura (db_, api_, json_) em camadas de domínio.
  8. Reveja nomes em PRs — eles comunicam tanto quanto o código em si.

7. Exemplo prático de refatoração

Um exemplo simples mostra o poder de nomes bem escolhidos.

Antes (difícil de entender):

def p(u, d):
    r = []
    for i in d:
        if i[1] == u:
            r.append(i[0])
    return r

Esse código até funciona, mas o leitor não sabe o que p, u, d ou r significam.

Depois (mesma lógica, nomes expressivos):

def get_orders_by_user(user_id: int, orders: list[tuple[int, int]]) -> list[int]:
    user_orders = []
    for order_id, owner_id in orders:
        if owner_id == user_id:
            user_orders.append(order_id)
    return user_orders

Sem mudar nada na lógica, o código agora se explica. Os nomes contam a história completa — o que está sendo filtrado, por quê e o que é retornado.

Conclusão

Dar bons nomes é mais do que estilo: é comunicação entre mentes técnicas. Variáveis bem nomeadas expressam intenção, reforçam arquitetura e tornam o código sustentável ao longo do tempo. O nome certo transforma a leitura em compreensão imediata — e isso é poder puro na engenharia de software.

Se este artigo te fez repensar como você nomeia variáveis, compartilhe com sua equipe ou continue a conversa no Mastodon: @riverfount@bolha.us

Espalhe boas práticas e ajude mais pessoas a escrever código que realmente se explica por si só.



Riverfount
Vicente Eduardo Ribeiro Marçal

A complexidade ciclomática mede o número de caminhos de execução independentes em uma função ou módulo Python, ajudando a identificar código difícil de testar e manter. Desenvolvida por Thomas J. McCabe em 1976, essa métrica é calculada como o número de pontos de decisão (if, for, while, etc.) mais um, revelando riscos em fluxos ramificados excessivos.

Mas o que é Complexidade Ciclomática?

Complexidade ciclomática (CC) quantifica a densidade de caminhos lógicos em um grafo de controle de fluxo. Em Python, cada estrutura condicional ou de loop adiciona ramificações: um if simples eleva a CC para 2, enquanto and/or em condições compostas multiplica caminhos independentes. A fórmula básica é CC = E - N + 2P, onde E são arestas, N nós e P componentes conectados, mas ferramentas como radon ou flake8 computam isso automaticamente.

E por que diminuir a CC importa para Pythonistas?

Código Python com CC alta (>10) aumenta o risco de bugs ocultos e eleva o custo de testes unitários para cobertura total. Funções longas com if-elif-else encadeados violam o Zen of Python (“Flat is better than nested”), complicando debugging em IDEs como PyCharm. Em microservices ou APIs Flask/FastAPI, CC elevada impacta deploy em Docker, pois refatorações viram gargalos em CI/CD.

Calculando CC em Código Python

Considere este exemplo problemático:

def processar_usuario(usuario, eh_admin=False, eh_pago=False):
    if not usuario:
        return None
    if eh_admin and eh_pago:
        return "acesso_total"
    elif eh_admin:
        return "acesso_admin"
    elif eh_pago:
        return "acesso_basico"
    else:
        if usuario.ativo:
            return "acesso_limitado"
        return "bloqueado"

Aqui, CC ≈ 6 devido a ramificações múltiplas. Use radon cc arquivo.py para medir:

processar_usuario: CC=6 (alto risco)

Interpretação e Limites Recomendados

Faixa de CC Nível de Risco Ação Sugerida
1-5 Baixo Manter como está
6-10 Moderado Refatorar se possível
11-20 Alto Dividir função imediatamente
>20 Crítico Refatoração urgente

Valores acima de 10 sinalizam antipadrões em Python, como god functions em Django views.

Estratégias de Redução em Python

  • Extraia funções puras: Divida em helpers como validar_usuario() e determinar_nivel_acesso().
  • Use polimorfismo: Substitua condicionais por classes com @dataclass ou Enum.
  • Guard clauses: Prefira if not condicao: return para early returns.
  • Strategy Pattern: Dicionários mapeiam condições a funções: handlers = {eh_admin: handler_admin}.
  • Ferramentas: Integre pylint ou mypy no pre-commit hook Git para alertas automáticos.

Exemplo refatorado (CC reduzida para 2):

def processar_usuario(usuario, eh_admin=False, eh_pago=False):
    if not usuario:
        return None
    return determinar_nivel_acesso(usuario.ativo, eh_admin, eh_pago)

def determinar_nivel_acesso(ativo, eh_admin, eh_pago):
    if eh_admin and eh_pago:
        return "acesso_total"
    handlers = {
        (eh_admin, eh_pago): "acesso_basico",
        eh_admin: "acesso_admin"
    }
    return handlers.get((eh_admin, eh_pago), "acesso_limitado" if ativo else "bloqueado")

Integração em Workflows Python

Em projetos com pytest, mire 100% branch coverage em funções CC<10. No VS Code, extensões como "Python Docstring Generator" ajudam na documentação pós-refatoração. Para equipes, thresholds no GitHub Actions bloqueiam merges com CC>15, alinhando com práticas DevOps em Kubernetes.

Monitore CC regularmente para código limpo e escalável em Python. Experimente radon no seu repo hoje e compartilhe comigo em @riverfount@bolha.us sua maior redução de CC!



Riverfount
Vicente Eduardo Ribeiro Marçal

Evitar números mágicos em expressões booleanas é uma recomendação explícita de linters Python modernos (como Pylint e Ruff, via regra PLR2004), pois esses valores dificultam a leitura e a manutenção do código. Entender essa regra e o contexto em que ela surgiu ajuda a justificar a prática ao time e a padronizar o estilo da base de código.

PLR2004: de onde vem essa regra?

A sigla PLR2004 é o identificador da regra magic-value-comparison em ferramentas de lint para Python, como o linter Ruff, que reutiliza a numeração herdada do Pylint. A regra é derivada diretamente da mensagem de refatoração R2004 – magic-value-comparison do Pylint, mantido pelo projeto PyCQA, que incentiva o uso de constantes nomeadas em vez de valores mágicos em comparações.

Na documentação do Ruff, a PLR2004 é descrita como uma verificação que detecta o uso de constantes numéricas “mágicas” em comparações, sugerindo substituí-las por variáveis constantes, justamente para melhorar legibilidade e manutenibilidade. A própria descrição enfatiza que o uso de valores mágicos é desencorajado pelas diretrizes de estilo PEP 8.

O que a PLR2004 considera um “magic value”

A regra PLR2004 inspeciona comparações como ==, !=, <, >, <= e >= em busca de literais numéricos sem nome, tratando-os como magic values quando representam algo além de números triviais. A documentação do Ruff destaca que esses valores tornam o código mais difícil de ler, pois o significado precisa ser inferido apenas pelo contexto, e recomenda o uso de constantes nomeadas.

Por conveniência, a regra costuma ignorar alguns valores muito comuns, como 0, 1 e "", que aparecem em operações idiomáticas, mas ainda assim permite configurar uma allowlist de valores aceitáveis para cenários específicos. Essa flexibilidade existe porque, em certos domínios, números como 90, 180 ou 360 deixam de ser “mágicos” e passam a ser parte da linguagem natural do problema (por exemplo, ângulos em graus).

Por que números mágicos atrapalham em expressões booleanas

Em expressões booleanas, o problema dos números mágicos fica mais evidente, porque a condição deveria comunicar a regra de negócio de forma clara. Ao escrever algo como if status == 2:, o leitor não sabe, de imediato, o que 2 representa: ativo, suspenso, cancelado?

A documentação do Pylint para magic-value-comparison / R2004 afirma que usar constantes nomeadas em vez de valores mágicos melhora a legibilidade e a manutenibilidade do código. Quando o valor de negócio muda (por exemplo, o status “ativo” deixa de ser 2 e passa a ser 3), o uso de literais espalhados exige uma busca manual sujeita a erro, enquanto uma constante única permite a mudança em um único ponto.

Exemplos em Python aplicando a PLR2004

Exemplo ruim: números mágicos em comparações

def can_access_admin_area(user_role: int) -> bool:
    # 1 = admin, 2 = editor, 3 = viewer
    return user_role == 1

Nesse caso, a PLR2004 sinalizaria o 1 como um magic value na comparação, sugerindo a extração para uma constante com nome significativo.

Exemplo melhor: constante nomeada

ADMIN_ROLE_ID = 1

def can_access_admin_area(user_role: int) -> bool:
    return user_role == ADMIN_ROLE_ID

Aqui, a expressão booleana se explica sozinha e a ferramenta de lint não acusa a regra PLR2004, pois o valor numérico está encapsulado em uma constante nomeada.[2][1]

Exemplo ruim: múltiplos valores mágicos

def is_valid_retry(status_code: int, retries: int) -> bool:
    # 200: OK; 500: erro interno; 3: máximo de tentativas
    return status_code != 200 and status_code != 500 and retries < 3

Esse padrão é exatamente o tipo de uso que a regra magic-value-comparison (PLR2004) se propõe a detectar.

Exemplo melhor: constantes de domínio

HTTP_OK = 200
HTTP_INTERNAL_ERROR = 500
MAX_RETRIES = 3

def is_valid_retry(status_code: int, retries: int) -> bool:
    return status_code not in (HTTP_OK, HTTP_INTERNAL_ERROR) and retries < MAX_RETRIES

Agora cada número tem um nome de domínio, a intenção da condição é clara e a manutenção futura fica concentrada nas constantes.

Exemplo com Enum para estados

from enum import Enum, auto

class UserStatus(Enum):
    INACTIVE = auto()
    ACTIVE = auto()
    SUSPENDED = auto()

def is_active(status: UserStatus) -> bool:
    return status is UserStatus.ACTIVE

Ao usar Enum, o código evita completamente comparações numéricas, eliminando o gatilho da PLR2004 e expressando a lógica booleana em termos de estados de negócio.

Conclusão: aproveite PLR2004 a seu favor

A regra PLR2004 (magic-value-comparison), definida originalmente no Pylint e incorporada pelo linter Ruff, existe justamente para forçar a substituição de números mágicos por constantes e construções semânticas em comparações. Em vez de encarar o aviso como ruído, é possível usá-lo como guia de refatoração para deixar suas expressões booleanas mais claras, consistentes e fáceis de evoluir.



Riverfount
Vicente Eduardo Ribeiro Marçal

Descubra como o pattern matching no Python 3.10+ transforma árvores de if/elif em código declarativo e poderoso, superando limitações do switch case clássico. Neste guia técnico para desenvolvedores Python, explore exemplos práticos de destructuring de listas, dicionários e classes, guards e padrões compostos – otimizado para buscas como “pattern matching Python tutorial”, “match case vs switch Python” e “structural pattern matching exemplos”.

O que Torna o Pattern Matching Único

Introduzido pelos PEPs 634, 635 e 636 no Python 3.10, o match/case vai além da comparação de valores: descreve a estrutura de dados, desconstruindo tuplas, listas, dicionários e objetos em variáveis prontas para uso. Diferente do switch case de C/Java, que compara apenas escalares sem fallthrough automático, aqui o primeiro case que casa encerra o bloco, eliminando bugs comuns. Ideal para APIs REST, eventos JSON e parsers em projetos full-stack Python.

Sintaxe Básica vs Switch Case

Exemplo clássico de dias da semana, similar a um switch mas com OR nativo (|) e wildcard (_):

def weekday_name(day: int) -> str:
    match day:
        case 1:
            return "Segunda-feira"
        case 2 | 3 | 4 | 5:
            return "Dia útil"
        case 6 | 7:
            return "Fim de semana"
        case _:
            raise ValueError(f"Dia inválido: {day}")

Sem break necessário – o case para automaticamente. Switch tradicional exigiria enum ou strings com fallthrough manual.

Destructuring: Poder Estrutural

O diferencial: padrões que capturam partes de estruturas compostas.

Tuplas e Listas

def process_point(point):
    match point:
        case (0, 0):
            return "Origem"
        case (0, y):
            return f"Eixo Y: {y}"
        case (x, 0):
            return f"Eixo X: {x}"
        case (x, y):
            return f"Ponto: ({x}, {y})"
        case [x, y, *rest]:
            return f"Lista longa: inicia {x},{y} + {len(rest)}"
        case _:
            raise TypeError("Formato inválido")

Captura variáveis diretamente, sem indexação manual – impossível em switch puro.

Dicionários e Eventos

def handle_event(event: dict):
    match event:
        case {"type": "click", "x": x, "y": y}:
            return f"Clique em ({x}, {y})"
        case {"type": "user", "id": uid}:
            return f"Usuário {uid}"
        case _:
            return "Ignorado"

Perfeito para payloads HTTP/JSON em Flask ou FastAPI.

Classes e Dataclasses

from dataclasses import dataclass

@dataclass
class CreateUser:
    email: str

@dataclass
class DeleteUser:
    id: int

def dispatch(cmd):
    match cmd:
        case CreateUser(email=email):
            return f"Criar: {email}"
        case DeleteUser(id=uid):
            return f"Excluir: {uid}"

Desconstrói atributos por nome – switch não acessa objetos assim.

Guards e Padrões Compostos

Combine matching com condições (if) e OR:

def classify(num: int):
    match num:
        case 0:
            return "Zero"
        case x if x > 0:
            return "Positivo"
        case x if x % 2 == 0:
            return "Par negativo"
        case _:
            return "Ímpar negativo."

Guards executam pós-captura, mantendo lógica coesa – superior a ifs externos em switches.

Vantagens sobre Switch Case

Aspecto Switch Case (C/Java) Pattern Matching Python
Comparação Valores escalares Estrutura + valores [4]
Destructuring Não Sim (listas/objetos) [1]
Guards/Condições Externo Integrado no case [2]
Fallthrough Manual (break) Automático [8]
Casos Múltiplos Labels separados `

Reduz if/elif verbosos em 50-70% para roteamento de dados.

Adote pattern matching em seus projetos Python para código mais legível e robusto. Teste os exemplos acima no seu ambiente e compartilhe comigo em @riverfount@bolha.us qual use case você vai aplicar primeiro. Para mais tutoriais avançados em Python, Spring Boot ou microservices, inscreva-se ou pergunte aqui!



Riverfount
Vicente Eduardo Ribeiro Marçal

Procurando por dicionários Python DSA, hash tables em Python, complexidade Big O dict Python ou estruturas de dados Python avançadas? Neste guia técnico desvendamos os princípios internos dos dicionários (dict), desde hashing e colisões até operações otimizadas para algoritmos reais. Ideal para engenheiros de software que buscam performance em microservices, grafos e entrevistas técnicas – leia e eleve seu código Python a outro nível!

Dicionários em Python (dict) são uma implementação eficiente de hash tables (tabelas de hash), uma estrutura de dados essencial em DSA para mapear chaves únicas a valores com acesso médio em tempo constante O(1). Essa performance os torna superiores a listas para operações de busca, inserção e deleção em cenários não ordenados, como caches, contagens de frequência ou representações de grafos. Desde Python 3.7, eles mantêm ordem de inserção, combinando benefícios de hash tables com listas ordenadas.[1]

Implementação Interna e Hashing

Internamente, o Python computa um hash da chave imutável (ex.: hash('chave')) para determinar o índice na tabela subjacente, um array redimensionável. Colisões são resolvidas por open addressing com probing quadrático. Chaves mutáveis (como listas) geram TypeError para evitar inconsistências.

Exemplo de hashing básico:

chave = 'abc'
hash_val = hash(chave)  # Resultado varia por sessão, ex: -123456789
print(f"Hash de '{chave}': {hash_val}")

Isso garante lookups rápidos, mas hashes ruins (ex.: ataques de hash-flooding) degradam para O(n) no pior caso.

Operações Fundamentais com Exemplos

Aqui estão as operações core, com análise de complexidade:

  • Criação e Inicialização:
  # Dict literal
  freq = {'a': 2, 'b': 1}
  
  # From iterable
  from collections import Counter
  freq = Counter('abacaxi')  # {'a': 3, 'b': 1, 'c': 1, 'x': 1, 'i': 1}
  • Inserção/Atualização (O(1) médio):

    freq['z'] = 1  # Insere ou atualiza
    freq.setdefault('y', 0)  # Insere só se ausente
    
  • Busca e Acesso (O(1) médio):

    valor = freq.get('a', 0)  # 3, com default se chave ausente
    if 'a' in freq:  # Verificação segura
      print(freq['a'])
    
  • Remoção (O(1) médio):

    del freq['z']  # Remove chave
    popped = freq.pop('b', None)  # Retorna valor ou default
    
  • Iteração Eficiente:

    # Chaves, valores ou itens
    for chave, valor in freq.items():
      print(f"{chave}: {valor}")
    

Complexidade Assintótica Detalhada

Operação Média (Amortizada) Pior Caso Notas
Inserção O(1) O(n) Redimensiona em load factor ~2/3
Busca (get) O(1) O(n) Colisões extremas
Deleção O(1) O(n) Marca como “tombstone”
Iteração O(n) O(n) Linear no tamanho
Len() O(1) O(1) Armazenado explicitamente

Boas Práticas e Casos de Uso em DSA

  • Evite chaves mutáveis: Use frozenset ou tuplas para chaves compostas.
  • Defaultdict para Simplicidade: python from collections import defaultdict graph = defaultdict(list) graph['A'].append('B') # Lista auto-criada
  • Aplicações:
    • Grafo de Adjacência: adj = {'A': ['B', 'C'], 'B': ['A']} para BFS/DFS.
    • Cache LRU Manual: Track acessos com dict + heapq.
    • Contagem de Frequência: Counter para anagramas ou sliding windows.
  • Alternativas: collections.OrderedDict para popitem(LRU), ou dict com __missing__ customizado.

Em projetos full-stack ou microservices, dicionários otimizam APIs REST (ex.: roteamento por ID) e automação, escalando para milhões de entradas sem gargalos.

Conclusão

Dominar dicionários é o primeiro passo para algoritmos escaláveis em Python – aplique esses conceitos hoje e veja seu código voar! Teste os exemplos no seu ambiente, experimente em LeetCode ou compartilhe em @riverfount@bolha.us como usou em projetos reais.



Riverfount
Vicente Eduardo Ribeiro Marçal

Generators em Python são funções especiais que usam yield para gerar valores sob demanda, economizando memória em vez de criar listas completas na RAM. Pense neles como “listas preguiçosas” que produzem um item por vez, ideais para processar arquivos grandes ou sequências infinitas sem travar o sistema.

Yield vs Return: A Diferença Fundamental

return encerra a função imediatamente após retornar um único valor, enquanto yield pausa a execução, retorna um valor e preserva o estado interno para continuar de onde parou na próxima chamada. Isso permite que uma única função gere múltiplos valores sequencialmente, como um loop “congelado” e retomado.

def com_return(n):
    for i in range(n):
        return i  # Para após o primeiro valor: sempre retorna 0

def com_yield(n):
    for i in range(n):
        yield i  # Gera 0, 1, 2... até n, pausando entre cada yield

print(next(com_return(5)))  # 0 (e função termina)
for i in com_yield(5):      # 0, 1, 2, 3, 4 (estado preservado)
    print(i)

return é para resultados finais únicos; yield constrói iteradores reutilizáveis.

Como Funcionam os Generators Básicos

Uma função generator parece normal, mas substitui return por yield, que pausa a execução e retorna um valor, preservando o estado para continuar depois. Isso cria um objeto iterável consumido com for ou next(), perfeito para loops sem alocar memória extra.

def contar_ate(n):
    for i in range(n):
        yield i

for numero in contar_ate(5):
    print(numero)  # Saída: 0 1 2 3 4

Vantagens em Memória e Performance

Generators brilham com dados grandes: uma lista de 1 milhão de números usa ~80MB, mas o generator equivalente consome apenas 128 bytes, gerando itens sob demanda. São nativos para for, list(), sum() e economizam tempo em cenários reais como leitura de logs ou CSV gigantes.

Expressões geradoras simplificam ainda mais: (x**2 for x in range(1000000)) cria um generator conciso sem parênteses extras, consumido iterativamente.

Tratamento de Exceções Simples

Exceções funcionam naturalmente dentro do generator. Use try/except para capturar erros durante a geração, como valores inválidos em um parser de JSON, mantendo o fluxo seguro.

def numeros_validos(arquivo):
    with open(arquivo) as f:
        for linha in f:
            try:
                yield int(linha.strip())
            except ValueError:
                print(f"Ignorando linha inválida: {linha}")
                continue

Isso previne crashes em dados reais “sujos”, comuns em automações e ETLs.

Fechamento Correto de Resources

Generators com arquivos ou conexões precisam de fechamento para evitar vazamentos. Use try/finally internamente ou generator.close() externamente, garantindo liberação automática.

# Exemplo 1: try/finally interno + close() externo
def ler_arquivo(arquivo):
    f = open(arquivo)
    try:
        for linha in f:
            yield linha.strip()
    finally:
        f.close()

gen = ler_arquivo('dados.csv')
try:
    for dado in gen:
        processar(dado)
finally:
    gen.close()

Context managers (with) integram perfeitamente para automação, eliminando necessidade de close() manual.

# Exemplo 2: Context manager with (mais limpo e automático)
def ler_arquivo_with(arquivo):
    with open(arquivo) as f:
        for linha in f:
            yield linha.strip()

# Uso simples e seguro - fecha automaticamente
for dado in ler_arquivo_with('dados.csv'):
    processar(dado)

Aplicações Práticas Iniciais

  • Arquivos gigantes: Leia linha por linha sem carregar tudo.
  • Sequências infinitas: Fibonacci ou contadores sem fim.
  • Pipelines simples: Filtre e transforme dados em cadeia.
  • Testes unitários: Mock de iteradores sem dados reais.​

Esses padrões otimizam ERPs, scripts de automação e APIs desde o primeiro projeto.

Conclusão

Generators transformam código Python em soluções eficientes e elegantes. Comece substituindo listas por generators em seus loops e veja a diferença em performance imediatamente.

Teste os exemplos acima no seu próximo projeto Python, meça o uso de memória com sys.getsizeof() e compartilhe seus resultados em @riverfount@bolha.us para discutirmos otimizações reais juntos!



Riverfount
Vicente Eduardo Ribeiro Marçal

Você já parou para pensar por que seu código Python consome cada vez mais memória em aplicações de longa duração, mesmo sem vazamentos óbvios? Palavras-chave como “garbage collector Python”, “contagem de referências Python”, “ciclos de referência Python” e “otimização de memória CPython” dominam buscas de desenvolvedores que enfrentam pausas inesperadas, inchaço de heap ou serviços que “incham” ao longo do tempo. Neste guia técnico expandido e atualizado, um engenheiro especialista em Python mergulha nos mecanismos internos do GC do CPython – com exemplos práticos de código, benchmarks reais e dicas avançadas de tuning – para você dominar a gestão de memória, detectar vazamentos sutis, configurar gerações otimizadas e escalar aplicações de produção sem surpresas.

Visão geral da memória no CPython

Python (implementação CPython) representa praticamente tudo como objetos alocados no heap: inteiros pequenos, strings, listas, funções, frames de pilha, módulos etc. Cada objeto PyObject carrega metadados essenciais, incluindo um contador de referências (ob_refcnt), tipo (ob_type) e, para contêineres rastreados pelo GC, ponteiros para listas duplamente ligadas das gerações (gc_next, gc_prev).

O ciclo de vida completo é: alocação via PyObject_New → incremento de refcount em referências → possível promoção geracional → detecção de ciclos ou refcount=0 → tp_dealloc (chama del se aplicável) → PyObject_Free. Objetos imutáveis como tuples pequenos ou strings interned podem ser otimizados pelo PGO (Python Object Generalizer), mas o GC foca em contêineres mutáveis.

CPython usa duas camadas complementares: contagem de referências (imediata, determinística) + GC geracional (para ciclos raros, probabilístico).

Contagem de referências: O coração do gerenciamento

O mecanismo primário é contagem de referências: Py_INCREF() em toda atribuição/nova referência, Py_DECREF() em remoções/saídas de escopo. Ao zerar, tp_dealloc é imediato, sem pausas.

  • Cenários de INCREF: atribuição (a = obj), inserção em lista/dict (lst.append(obj)), passagem por parâmetro, cópia forte.
  • Cenários de DECREF: del var, variável sai de escopo, lst.pop(), dict del, ciclo de vida de frames locais termina.
  • Overhead baixo: Operações atômicas em 64-bit, mas visíveis em workloads intensivos (ex.: loops com listas mutáveis).

Exemplo simples de contagem de referências

import sys
import tracemalloc

tracemalloc.start()  # Para medir alocações reais

a = []          # PyList_New → refcount=1
print(f"Refcount inicial: {sys.getrefcount(a)}")  # >=2 (a + getrefcount)

b = a           # Py_INCREF → refcount=2
print(f"Após b=a: {sys.getrefcount(a)}")

del b           # Py_DECREF → refcount=1
print(f"Após del b: {sys.getrefcount(a)}")

del a           # Py_DECREF → 0 → tp_dealloc imediato
print(tracemalloc.get_traced_memory())  # Memória liberada
tracemalloc.stop()

sys.getrefcount infla +1 pela referência temporária da função. Use ctypes para refcount “puro” se necessário.

O problema: ciclos de referência e por que falham

Contagem falha em ciclos: A→B→A mantém refcounts >0 mutuamente, mesmo se o “root” externo foi deletado. Comum em árvores bidirecionais, caches LRU com backrefs, grafos.

Exemplo expandido de ciclo com diagnóstico

import gc
import weakref

class Node:
    def __init__(self, value):
        self.value = value
        self.next = None
        self._weak_next = None  # Weakref para evitar ciclo artificial

a = Node(1)
b = Node(2)

a.next = b
b.next = a  # Ciclo forte

print(f"Antes del - refcount a: {sys.getrefcount(a)}, b: {sys.getrefcount(b)}")
del a, b
print(f"Após del - ainda vivos! refcount a: {sys.getrefcount(a)}, b: {sys.getrefcount(b)}")

Weakrefs (unilateral) quebram ciclos sem custo de GC: weakref.ref(other)().

Coletor geracional: Detecção probabilística de ciclos

CPython adiciona GC apenas para contêineres (list, dict, instances, sets, etc. com tp_traverse/tp_clear). Não rastreia ints, floats, strings. Hipótese geracional: 90%+ objetos morrem jovens; sobreviventes são longevos.

4 Gerações reais (não documentadas publicamente): gen0 (jovem), gen1, gen2 (velhas), permanent (estáticas como módulos). Novos contêineres vão para gen0 via PyList_Append etc.

Limiares e disparo expandido

Thresholds padrão: (700, 10, 10) – gen0 dispara a cada ~700 alocs líquidas; a cada 10 coletas gen0, coleta gen1; a cada 10 gen1, full GC. Incremental GC (Python 3.13+?) limpa frações da gen2 por ciclo.

import gc

print("Thresholds:", gc.get_threshold())  # (700, 10, 10)
print("Contagens atuais:", gc.get_count())  # (gen0, gen1, gen2)

gc.set_threshold(1000, 20, 20)  # Menos pausas para throughput
print("Novos thresholds:", gc.get_threshold())

Sobreviventes sobem geração; gen2 é “quase permanente”.

Algoritmo de detecção de ciclos: Mark & Sweep otimizado

Fases (gc_collect_region):

  1. Roots → Reachability: De raízes (globals, stack, registers), traverse contêineres marcando alcançáveis (BFS via tp_traverse).
  2. Tentative Unreachable: Move suspeitos para lista separada; re-traverse para reviver falsos positivos.
  3. Finalizers & Clear: Chama tp_finalize/tp_clear em ciclos confirmados; DEALLOC se possível.

Ciclos puramente internos são liberados mesmo com refcount>0. gc.garbage guarda “uncollectable” com del pendente.

Usando o módulo gc na prática avançada

Monitoramento e debugging em produção

import gc
import objgraph  # pip install objgraph (opcional, para histograms)

print("Stats por geração:", gc.get_stats())
print("Objetos rastreados:", len(gc.get_objects()))

# Simular ciclo e coletar
def create_cycles(n=100):
    for _ in range(n):
        a = []; b = []; a.append(b); b.append(a)

create_cycles()
print(f"Antes GC: {len(gc.get_objects())}")
collected = gc.collect(2)  # Full GC
print(f"Coletados: {collected}, Garbage: {len(gc.garbage)}")
# objgraph.show_most_common_types()  # Se instalado

gc.callbacks para hooks em coletas; gc.is_tracked(obj) para checar.

Exemplo: Hunting vazamentos em loop

import gc
import time

def leaky_loop():
    cache = {}
    while True:
        obj = {'data': list(range(10000))}  # Simula dados grandes
        cache[id(obj)] = obj  # "Vazamento" intencional
        if len(gc.get_objects('dict')) > 10000:
            gc.collect()
            print("GC forçado, objetos:", len(gc.get_objects()))

# Em produção: rode com gc.set_debug(gc.DEBUG_LEAK)

Ajustando o GC para workloads específicos

Alto throughput (FastAPI/Flask): Thresholds altos + disable em endpoints rápidos. Data/ML (Pandas/NumPy): Desabilite GC (NumPy usa refcount puro). Long-running (Celery/services): Monitore gc.get_count(); tune baseado em RSS.

import gc
import signal

def tune_gc():
    gc.disable()  # Para bursts
    gc.set_threshold(0, 0, 0)  # Desabilita thresholds

# Context manager custom
class GCOff:
    def __enter__(self):
        gc.disable()
    def __exit__(self, *args):
        gc.enable()
        gc.collect()

Benchmarks reais: GC pausas <1ms em gen0; full GC pode ser 10-100ms em heaps grandes – meça com gc.get_stats().

Pontos práticos para engenheiros Python

  • **Evite del em ciclos**: Use contextlib ou weakrefs; del bloqueia auto-liberação.
  • Context managers > GC: Sempre with open(), with conn: para files/sockets – GC não garante ordem/timing.
  • **Slots e slots**: Reduzem dict interno (economia ~20-30% memória em classes).
  • Prod monitoring: Integre memory_profiler, fil, gc.get_objects() em healthchecks.
  • PyPy/Jython: Diferentes GCs (tracing); migração requer re-tune.

Conclusão e Próximos Passos

Dominar o GC do CPython transforma você de “esperando pausas” para “engenheiro proativo”: thresholds tunados cortam latência 20-50%, weakrefs eliminam 90% ciclos, e monitoramento previne OOMs em prod. Teste esses exemplos no seu código agora – rode gc.set_debug(gc.DEBUG_STATS) e veja o impacto real.



Riverfount
Vicente Eduardo Ribeiro Marçal

Baseado na Live de Python #150 do canal Eduardo Mendes no YouTube, este artigo explora de maneira prática e direta a “primitive obsession” — code smell onde tipos primitivos (strings, dicts, lists) substituem abstrações de domínio ricas — e como dataclasses (Python 3.7+, PEP 557) oferecem solução definitiva para criar Value Objects tipados, imutáveis e comportamentalmente ricos. Para sêniores buscando elevar modelagem DDD e reduzir technical debt em escala.

Primitive Obsession: Raiz do Problema

Primitive obsession ocorre quando entidades de domínio são reduzidas a “bags of primitives”, i. e., em que objetos de domínio são representados apenas como coleções simples de tipos primitivos (como strings, números, listas e dicionários) sem encapsulamento ou comportamento. Ou seja, em vez de ter classes ou estruturas que representem conceitos ricos com regras, validações e métodos, o código manipula “sacos” ou “pacotes” de dados primitivos soltos, o que aumenta a complexidade, propensão a erros e dispersa a lógica de negócio. Aplicando princípios de POO fundamentais: encapsulamento, polimorfismo e responsabilidade única. Consequências incluem:

  • Lógica de domínio espalhada: Funções utilitárias fazem parsing manual de strings/dicts
  • Falta de invariants: Sem validação, DDDs inválidos ou CPFs malformados passam despercebidos
  • Sem comportamentos: nome_completo() vira função externa poluente
  • Type unsafety: KeyError em runtime, mypy cego para estruturas aninhadas
  • Testabilidade pobre: Mockar dicts vs mockar objetos com dependências claras
# ANTES: Primitive Obsession Clássica
dados = [
    {"nome": "João", "sobrenome": "Silva", "telefone": {"residencial": "1234-5678"}, "ddd": 11},
    {"nome": "Maria", "sobrenome": "Santos", "telefone": {"residencial": "9876-5432"}, "ddd": 99}  # DDD inválido!
]

# Lógica espalhada + runtime errors
def nome_completo(dado: dict) -> str:
    return f"{dado['nome']} {dado['sobrenome']}"

def telefone_residencial(dado: dict) -> str:
    return dado['telefone']['residencial']  # KeyError se faltar!

nomes = [nome_completo(d) for d in dados]
telefone = telefone_residencial(dados[0])  # Fragile!

Custo real: Cyclomatic complexity explode, coverage cai, onboarding demora 3x mais.[9][10][1]

Dataclasses: Antídoto Completo à Primitive Obsession

Dataclasses não são “namedtuples mutáveis”. São geradores de Value Objects que restauram domínio perdido via automação OO inteligente:

1. Encapsulamento Automático + Comportamento

from dataclasses import dataclass, field
from typing import ClassVar
from datetime import date

@dataclass(frozen=True)
class Telefone:
    residencial: str
    
    def formatado(self, ddd: int) -> str:
        return f"({ddd}) {self.residencial}"

@dataclass(frozen=True)
class Pessoa:
    nome: str
    sobrenome: str
    telefone: Telefone
    ddd: int
    data_nascimento: date
    
    # Invariant via post_init
    def __post_init__(self):
        if self.ddd < 11 or self.ddd > 99:
            raise ValueError(f"DDD inválido: {self.ddd}")
        if self.data_nascimento > date.today():
            raise ValueError("Data futura inválida")
    
    @property
    def nome_completo(self) -> str:
        return f"{self.nome} {self.sobrenome}"
    
    @property
    def telefone_formatado(self) -> str:
        return self.telefone.formatado(self.ddd)
    
    @property
    def idade(self) -> int:
        return (date.today() - self.data_nascimento).days // 365
    
    # Value Object equality por valor, não referência
    def __eq__(self, other):
        if not isinstance(other, Pessoa):
            return NotImplemented
        return (self.nome_completo, self.ddd) == (other.nome_completo, other.ddd)

2. Uso: Domínio Restaurado, Zero Boilerplate

# Value Objects puros, type-safe
pessoas = [
    Pessoa("João", "Silva", Telefone("1234-5678"), 11, date(1990, 5, 15)),
    Pessoa("Maria", "Santos", Telefone("9876-5432"), 21, date(1985, 8, 22))
]

# Comportamento encapsulado, sem loops manuais
print([p.nome_completo for p in pessoas])
# ['João Silva', 'Maria Santos']

print(pessoas[0].telefone_formatado)  # "(11) 1234-5678"
print(pessoas[0].idade)  # ~35

# Validação em tempo de construção
try:
    Pessoa("Inválido", "X", Telefone("0000"), 5, date(2026, 1, 1))
except ValueError as e:
    print(e)  # "DDD inválido: 5"

3. Integrações Avançadas

from dataclasses import asdict, astuple
import json
from typing import Any

# Serialização controlada (não expõe internals)
def pessoa_to_json(p: Pessoa) -> str:
    return json.dumps({
        "nome_completo": p.nome_completo,
        "telefone": p.telefone_formatado,
        "idade": p.idade
    })

# mypy + Pydantic validation
# pip install pydantic[email-validator]
from pydantic import BaseModel, validator

class PessoaPydantic(BaseModel):
    nome_completo: str
    telefone: str
    idade: int
    
    @validator('telefone')
    def validate_telefone(cls, v):
        if not v.startswith('('):
            raise ValueError('Formato inválido')
        return v

Benefícios Arquiteturais Profundos

Aspecto Primitive Obsession Dataclasses Value Objects
Encapsulamento 0% (dicts públicos) 100% (métodos + invariants)
Type Safety Runtime KeyError Compile-time + runtime
Testabilidade Mock dicts complexos Mock objetos com dependências claras
Performance ~1x (dict access) ~1.2x (dataclass overhead mínimo)
Serialização json.dumps(dict) asdict() + versionamento
Extensibilidade Refatorar todos consumers[¹] Herança + composição

[1] Dificuldade que ocorre quando a estrutura de dados primitivos é modificada ou evoluída.

Métricas reais: Times reportam 40% menos bugs de dados, 25% mais velocidade em reviews.

Primitive Obsession em Escala: O Verdadeiro Custo

# Microsserviço com 50+ endpoints primitivos
@app.get("/clientes")
def listar_clientes():
    return [
        {"id": 1, "nome": "João", "email": "joao@email.com", "ativo": True},
        # 100+ campos espalhados, validação em middlewares...
    ]

Virada com dataclasses:

@dataclass(frozen=True)
class Cliente:
    id: int
    nome_completo: str
    email: EmailStr  # pydantic
    status: ClienteStatus  # Enum
    
    @classmethod
    def from_db(cls, row: Row) -> "Cliente":
        return cls(
            id=row.id,
            nome_completo=...,
            email=row.email,
            status=ClienteStatus.from_str(row.status)
        )

Conclusão

Dataclasses eliminam primitive obsession restaurando domínio rico sem sacrificar performance ou a experiência do desenvolvedor. São o “sweet spot” entre namedtuples (imutáveis, sem comportamento) e classes manuais (boilerplate pesado).



Riverfount
Vicente Eduardo Ribeiro Marçal

A arquitetura hexagonal, ou Ports and Adapters, coloca a lógica de negócio no centro de um hexágono simbólico, cercada por portas (interfaces abstratas) que conectam adaptadores externos como bancos de dados, APIs web, filas ou serviços de terceiros. Proposta por Alistair Cockburn em 2005, ela inverte as dependências tradicionais: o domínio não conhece frameworks ou persistência, mas estes dependem dele via injeção de dependências, promovendo código limpo e adaptável em Python. Essa abordagem alinha-se perfeitamente à filosofia “simples é melhor” do Python, mas com rigor para domínios complexos.

Componentes e Fluxo de Dados

O domínio abriga entidades imutáveis, agregados e regras puras, livres de anomalias arquiteturais como dependências de frameworks ou bancos de dados. Portas de entrada definem casos de uso (ex.: CreateUserPort), enquanto portas de saída expõem repositórios abstratos (ex.: UserRepository). Adaptadores concretos — como FastAPI para HTTP ou SQLAlchemy para DB — implementam essas portas, garantindo que o core permaneça intocado por mudanças externas. O fluxo entra pelas bordas, atinge o domínio via portas e sai por adaptadores, criando uma barreira unidirecional contra acoplamento.

Vantagens em Aplicações Python

  • Testes isolados e rápidos: Mockar portas permite TDD sem infraestrutura real, reduzindo flakiness em CI/CD com pytest.
  • Flexibilidade tecnológica: Troque Flask por FastAPI ou SQLite por DynamoDB sem refatorar o core, ideal para microsserviços.
  • Escalabilidade e manutenção: Suporta DDD em equipes grandes.
  • Longevidade: Evolução sem rewrites totais, perfeita para ERPs ou APIs corporativas em Python.

Desvantagens e Limitações

  • Curva de aprendizado e boilerplate: Abstrações iniciais sobrecarregam protótipos ou CRUD simples, violando “menos é mais” em Python.
  • Over-engineering em projetos triviais: Para apps sem domínio rico, aumenta complexidade sem ROI; prefira MVC tradicional.
  • Manutenção de portas: Muitas interfaces podem virar “abstrações vazias” se não gerenciadas, confundindo desenvolvedores menos experientes.

Casos de Uso Práticos em Python

Adote em microsserviços serverless. Útil em sistemas DDD como bibliotecas de gestão ou ERPs, isolando regras fiscais de persistência multi-DB. Evite em scripts ou MVPs rápidos; combine com Clean Architecture para monolitos legados. Exemplos reais incluem APIs de usuários com FastAPI, em que trocar mocks por Redis em produção é trivial.

Refatorando Projetos Existentes para Hexagonal

Refatorar monolitos Python para hexagonal exige abordagem incremental, priorizando estabilidade e testes. Siga estes passos práticos:

  1. Mapeie o Domínio Atual: Identifique entidades principais (ex.: User, Order) e regras de negócio misturadas em controllers/services. Extraia para domain/entities/ e domain/services/, criando dataclasses imutáveis.
  2. Defina Portas Mínimas: Para cada operação externa (DB, email, HTTP), crie interfaces ABC em domain/ports/ (ex.: UserRepository, EmailPort). Comece com 2-3 portas críticas.
  3. Crie Casos de Uso: Migre lógica de controllers para application/use_cases/ injetando portas. Exemplo: CreateUserUseCase(repo: UserRepository) orquestra validação e domínio.
  4. Implemente Adaptadores Gradualmente: Refatore controllers existentes para usar casos de uso (adaptadores de entrada). Crie infrastructure/repositories/ com implementações atuais (SQLAlchemy → PostgresUserRepository).
  5. Reestruture Pastas: Adote src/domain/, src/application/, src/infrastructure/. Use dependency-injector para wiring automático em main.py.
  6. Testes como Rede de Segurança: Escreva testes para portas/mock antes de refatorar, garantindo 100% de cobertura no domínio. Rode pytest em paralelo durante a migração.
  7. Migre por Feature: Refatore uma entidade por sprint (ex.: só User primeiro), mantendo código legado rodando via adaptadores híbridos.youtube​

Use a ferramenta radon para monitorar a complexidade ciclomática do código (ex.: radon cc src/). Se a média subir acima de 10 ou o ROI (retorno sobre investimento) cair — medido por testes mais lentos ou refatorações frequentes —, pause a migração. Projetos grandes (>10k linhas de código) recuperam o investimento em 3-6 meses com testes mais rápidos e manutenção simplificada; para projetos pequenos (<5k linhas), calcule o custo-benefício antes de prosseguir.

Conclusão: Adote Hexagonal e Eleve Seu Código

A arquitetura hexagonal transforma aplicações Python em sistemas resilientes, testáveis e evolutivos, isolando o domínio de frameworks voláteis e promovendo baixa manutenção a longo prazo. Ideal para domínios complexos como ERPs ou microsserviços, ela equilibra a simplicidade zen do Python com escalabilidade enterprise.​

Pronto para refatorar? Comece hoje: pegue um módulo CRUD do seu projeto, extraia as portas em 1 hora e teste com pytest. Baixe repositórios de exemplo no GitHub, experimente em um branch e veja a diferença em testes isolados. Compartilhe conosco no Mastodon em @riverfount@bolha.us sua primeira vitória hexagonal — ou dúvidas para discutirmos.



Riverfount
Vicente Eduardo Ribeiro Marçal

Você já precisou compartilhar um script Python com colegas e teve que explicar: “Instala o Python 3.12, cria um venv, instala requests e rich, depois roda”? Com o gerenciador UV, isso acabou.

Agora é possível escrever um único arquivo .py que já traz suas dependências dentro dele, como requests<3 e rich, e rodar tudo com apenas uv run script.py. Neste guia, você vai aprender como usar o bloco # /// script do UV para transformar scripts comuns em artefatos autocontidos, reprodutíveis e portáteis — perfeitos para automações, ferramentas internas e protótipos.

1. O que é o bloco # /// script

O UV entende um cabeçalho especial em arquivos Python, baseado no PEP 723, que permite declarar dependências diretamente no script. Ele tem esse formato:

# /// script
# dependencies = [
#   "nome-do-pacote>=versão",
#   "outro-pacote<versão",
# ]
# ///

Esse bloco é um TOML comentado, então o Python ignora, mas o UV o lê como metadados do script. Ele define:

  • Quais pacotes são necessários (dependencies).
  • A versão mínima de Python (requires-python).
  • O índice de pacotes, se for diferente do PyPI (index-url).

Com isso, o script vira um “mini-projeto” que carrega suas próprias dependências.

2. Instalando o UV (pré-requisito)

Antes de tudo, instale o UV no seu sistema:

# Linux/macOS
curl -LsSf https://astral.sh/uv/install.sh | sh

# Windows (PowerShell)
irm https://astral.sh/uv/install.ps1 | iex

Depois, verifique se está tudo certo:

bash
uv --version

Se aparecer a versão, o UV está pronto para usar.

3. Criando um script com dependências embutidas

Vamos criar um exemplo prático: um script que faz uma requisição HTTP com requests e mostra a saída formatada com rich.

Crie um arquivo http_client.py com o seguinte conteúdo:

#!/usr/bin/env -S uv run --script

# /// script
# requires-python = ">=3.12"
# dependencies = [
#   "requests>=2.31.0,<3",
#   "rich>=13.0.0",
# ]
# ///

import requests
from rich.console import Console
from rich.table import Table

console = Console()

def main():
    url = "https://httpbin.org/json"
    console.print(f"[bold blue]Fazendo GET em {url}...[/bold blue]")

    try:
        response = requests.get(url, timeout=10)
        response.raise_for_status()
        data = response.json()

        table = Table(title="Dados retornados")
        table.add_column("Chave")
        table.add_column("Valor")

        for key, value in data.items():
            table.add_row(str(key), str(value))

        console.print(table)
    except requests.RequestException as e:
        console.print(f"[bold red]Erro na requisição: {e}[/bold red]")

if __name__ == "__main__":
    main()

Esse script já traz:

  • Um shebang que usa uv run --script para executar diretamente.
  • Um bloco # /// script com requests e rich como dependências.
  • Um código real que usa essas bibliotecas para mostrar dados em uma tabela bonita.

4. Rodando o script pela primeira vez

Na pasta onde está http_client.py, execute:

uv run http_client.py

Na primeira execução, o UV vai:

  1. Ler o bloco # /// script.
  2. Criar um ambiente isolado para esse script.
  3. Instalar requests e rich (e suas dependências) nesse ambiente.
  4. Executar o script dentro desse ambiente.

Nas execuções seguintes, ele reutiliza o ambiente e roda direto, sem instalar nada de novo.

5. Adicionando dependências com uv add --script

Se precisar adicionar mais uma biblioteca (por exemplo, typer para CLI), use:

uv add --script http_client.py typer

Esse comando atualiza automaticamente o bloco # /// script no topo do arquivo, adicionando "typer" à lista de dependências. Depois disso, basta importar e usar typer no código.

6. Tornando o script executável (opcional)

Para rodar o script diretamente como um comando, torne-o executável:

chmod +x http_client.py

Agora é possível executá-lo sem chamar uv run explicitamente:

./http_client.py

O shebang #!/usr/bin/env -S uv run --script garante que o UV será usado para rodar o script com o ambiente correto.

7. Quando usar esse padrão

Esse modelo é ideal para:

  • Scripts de automação interna (CI, deploy, backup, etc.).
  • Ferramentas de linha de comando que circulam entre times.
  • Protótipos e PoCs que precisam de bibliotecas externas.
  • Scripts que você quer rodar em servidores sem configurar projeto completo.

Evite usar isso para:

  • Aplicações grandes com múltiplos módulos e estrutura de projeto.
  • Projetos que já usam pyproject.toml e uv init.

8. Dicas para produção

  • Sempre declare versões mínimas e máximas (requests>=2.31.0,<3) para evitar quebras.
  • Use requires-python para garantir que o script não rode em versões incompatíveis.
  • Em CI/CD, prefira usar uv sync em projetos completos, mas mantenha scripts autocontidos para tarefas pontuais.
  • Se quiser lock exato, o UV pode gerar um script.py.lock para fixar versões exatas.

Conclusão

Com o bloco # /// script do UV, scripts Python deixam de ser “códigos soltos” e viram artefatos autocontidos: trazem suas dependências, versão de Python e ambiente embutidos.

Basta instalar o UV, escrever o cabeçalho # /// script e rodar com uv run. O resultado é mais produtividade, menos setup manual e scripts que “simplesmente funcionam” em qualquer máquina com UV instalado.



Riverfount
Vicente Eduardo Ribeiro Marçal