Riverfount

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

O UV é um gerenciador de pacotes e projetos Python extremamente rápido, escrito em Rust, que substitui ferramentas como pip, venv e pipenv por comandos simples e automação de ambientes virtuais. Ele conecta gerenciamento de versões do Python, instalação de dependências e execução de scripts em um único comando, proporcionando agilidade no desenvolvimento.​

Instalação Rápida

Para começar, instale o UV facilmente via terminal:

  • Linux/macOS: curl -LsSf https://astral.sh/uv/install.sh | sh
  • Windows PowerShell: irm https://astral.sh/uv/install.ps1 | iex

Confirme a instalação com uv --version para garantir que está pronto para uso.

Criando e Executando Projetos

Com uv init nome_do_projeto, você cria um projeto Python completo com estrutura padrão, pyproject.toml, README.md, .gitignore e ambiente virtual configurado automaticamente. Navegue para a pasta (cd nome_do_projeto) e rode scripts diretamente usando:

uv run nome_do_script.py

Esse comando executa o script dentro do ambiente virtual gerenciado pelo UV, sem necessidade de ativação manual, mantendo o isolamento e limpeza do ambiente, ideal para aplicações Flask ou qualquer projeto Python.​

Gerenciando Dependências

Adicione pacotes facilmente:

uv add requests flask numpy

Esses comandos instalam as dependências dentro do ambiente virtual do projeto, gerenciam o arquivo pyproject.toml e criam um arquivo de bloqueio uv.lock para garantir ambientes reproduzíveis. Para remover pacotes, use uv remove nome_pacote, e para atualizar use uv update.​

Ambientes Virtuais e Comandos Úteis

O UV cria e gerencia ambientes virtuais automaticamente, mas você também pode criar manualmente com:

uv venv --python 3.12

Além disso, você pode listar dependências (uv list), sincronizar ambiente (uv sync) e executar ferramentas CLI do Python com uvx nome_da_ferramenta sem instalar globalmente.

O destaque é o comando uv run, que, além de executar scripts simples, pode ser usado para executar projetos Flask, testes ou qualquer outro comando Python garantindo que tudo rode no ambiente adequado e isolado do sistema.​

Para Aprofundar

Esta é uma visão introdutória do UV, focada nos conceitos básicos para iniciantes. Para explorar mais recursos avançados, personalização e exemplos detalhados, recomenda-se a leitura da documentação oficial, que está bem completa e constantemente atualizada em https://docs.astral.sh/uv/. Assim, você poderá aproveitar todo o potencial dessa poderosa ferramenta para gerenciar seus projetos Python com eficiência e facilidade.



Riverfount
Vicente Eduardo Ribeiro Marçal

Desde sua introdução oficial na versão 3.10 do Python, o pattern matching trouxe uma maneira estruturada e expressiva de lidar com fluxos condicionais complexos, especialmente ao trabalhar com dados estruturados como tuplas, listas, dicionários ou objetos. Para desenvolvedores experientes, explorar seus recursos avançados—como guards, padrões combinados OR/AND—e compreender suas limitações é fundamental para escrever códigos claros, eficientes e robustos.

Guardas: Condições Dinâmicas para Refinar Combinações

Guards são cláusulas condicionais if adicionadas depois do padrão básico, que só são avaliadas caso o pattern inicial case com o dado. Eles permitem filtrar ainda mais a correspondência com expressões arbitrárias.

Exemplo avançado classificando números com guards para regras detalhadas:

def classificar_numero(x):
    match x:
        case int() if x < 0:
            return "Número inteiro negativo"
        case int() if x == 0:
            return "Zero"
        case int() if x % 2 == 0:
            return "Número inteiro par"
        case int():
            return "Número inteiro ímpar"
        case float() if x.is_integer():
            return "Float que representa um inteiro"
        case float():
            return "Número float"
        case _:
            return "Não é um número"

Guards melhoram a legibilidade evitando ifs aninhados espalhados e permitem separar lógica granular enquanto mantêm a clareza do fluxo de decisão.

Combinações com Padrões OR e AND

Python permite combinar múltiplos padrões num mesmo case usando o operador | (OR). Isso evita duplicação de código quando múltiplos padrões devem resultar na mesma ação.

Exemplo com Enum usando OR para agrupar casos:

from enum import Enum

class Status(Enum):
    OK = 1
    WARNING = 2
    ERROR = 3

def tratar_status(status):
    match status:
        case Status.OK | Status.WARNING:
            print("Status aceitável")
        case Status.ERROR:
            print("Erro detectado")

Não existe operador direto para AND nos padrões; o comportamento AND deve ser implementado via guards, combinando várias condições:

def avaliar_evento(event):
    match event:
        case {"type": "click", "button": btn} if btn in ("left", "right"):
            print(f"Clique {btn} detectado")
        case _:
            print("Outro evento")

Limitações e Armadilhas Técnicas

  • Ordem dos casos é crucial: O match vai do topo para baixo, executando o primeiro case que casar. Padrões genéricos deveriam vir após os específicos para evitar capturas indesejadas.

  • Guards são avaliados após binding: Variáveis do pattern são ligadas antes do guard rodar. Mesmo que o guard retorne falso, as variáveis já foram criadas, podendo ter efeitos colaterais se seus valores forem usados inadvertidamente.

  • Sem fall-through: Cada case executa exclusivamente, diferente de switch/case em outras linguagens que permitem cair no próximo case.

  • Um único * por desempacotamento: A sintaxe não permite múltiplos nomes com * para capturar “resto” em padrões de sequência, limitando certas formas de combinar ou expandir sequências.

  • Complexidade prejudica legibilidade: O uso intensivo de múltiplos guards, ORs e outras combinações pode gerar padrões difíceis de entender e manter. Muitas vezes, dividir a lógica em funções auxiliares é preferível.

  • Guards com múltiplos padrões OR: Um guard não pode aplicar uma condição que dependa de múltiplos padrões OR simultaneamente, pois o binding de variáveis pode ocorrer em somente um dos padrões, causando inconsistência ou erros. Exemplo problemático:

def processar_tupla(t):
    match t:
        case (x, y) | (y, x) if x == y:
            print("Tupla simétrica")  # NÃO FUNCIONA como esperado
        case _:
            print("Outro caso")

Foi necessário replicar a lógica para casos diferentes ou desestruturar a lógica para manter clareza e correção.

Conclusão

Dominar pattern matching avançado com guards e padrões OR/AND eleva o nível do seu código Python, dando expressividade e eliminando estruturas condicionalmente complexas. Conhecer suas nuances e limitações ajuda a evitar armadilhas comuns que podem levar a bugs difíceis ou código difícil de manter, tornando seu código mais elegante, legível e eficiente diante de fluxos de dados complexos.



Riverfount
Vicente Eduardo Ribeiro Marçal

Em desenvolvimento de APIs REST, status codes HTTP são tão importantes quanto o payload da resposta. Eles comunicam, de forma padronizada, o resultado de cada requisição e são consumidos por clientes, gateways, observabilidade e ferramentas de monitoração. Apesar disso, ainda é comum encontrar código repleto de “números mágicos”, como 200, 404 ou 500 espalhados pela base.

Uma abordagem mais robusta é substituir esses valores literais por constantes descritivas, como HTTP_200_OK ou HTTP_404_NOT_FOUND. Essa prática aproxima o código das boas práticas de engenharia de software e melhora diretamente a legibilidade, a manutenção e a confiabilidade da API.

Constantes de status HTTP em Python

Frameworks modernos como FastAPI, Django REST Framework e outros já fornecem coleções de constantes ou enums para representar status codes HTTP. Isso cria um vocabulário padrão no código, evita ambiguidade e reduz dependência de “decorar” números.

Quando o framework ou stack utilizado não oferece esse mapeamento, é altamente recomendável criar o seu próprio módulo de constantes. Um exemplo simples é a criação do arquivo helper/status_code.py, que centraliza os códigos mais utilizados pela sua API. Esse arquivo funciona como um contrato semântico para a aplicação e pode ser adotado como padrão de time.

Exemplo de uso em código:

from helper import status_code

if response.status_code == status_code.HTTP_200_OK:
    print("Requisição bem-sucedida!")

Esse padrão é facilmente reconhecível por qualquer desenvolvedor que já tenha trabalhado com APIs, independentemente do backend ou da linguagem.

Legibilidade e intenção clara do código

Legibilidade é um dos pilares de um código de qualidade. Quando a base utiliza HTTP_201_CREATED em vez de 201, a intenção da resposta fica explícita.

Em uma revisão de código (code review), não é necessário parar para lembrar o que cada número significa. A constante descreve o comportamento desejado e reduz o esforço cognitivo de quem lê. Em times grandes ou em projetos que passam por muitas mãos, essa economia de contexto se traduz em menos dúvidas, menos ruído em revisões e onboarding mais rápido de novos membros.

Manutenção, consistência e redução de erros

Espalhar números mágicos na base aumenta o risco de inconsistência. É fácil um endpoint retornar 200 em um caso, 201 em outro cenário similar, ou ainda alguém digitar 2040 em vez de 204.

Com um módulo de constantes, você:

  • Centraliza a definição dos status codes.
  • Garante consistência sem depender da memória individual.
  • Facilita refinos pontuais de semântica (por exemplo, trocar um 200 por 204 quando a API deixa de retornar corpo em uma deleção).

Embora os valores dos status codes HTTP sejam padronizados e raramente mudem, o mapeamento semântico dentro da sua aplicação pode evoluir. Ter isso encapsulado em constantes torna essa evolução muito menos dolorosa.

Documentação viva e apoio das ferramentas

Constantes descritivas funcionam como documentação viva embutida no código. Em vez de manter uma tabela à parte em uma wiki, o próprio módulo de status codes evidencia quais valores são usados e com qual propósito.

Além disso, IDEs e ferramentas de desenvolvimento conseguem:

  • Sugerir autocompletar para HTTP_4xx ou HTTP_5xx.
  • Ajudar na navegação (go to definition) para entender onde e como os códigos são definidos.
  • Aumentar a segurança estática, evitando valores inválidos.

Tudo isso contribui para um fluxo de desenvolvimento mais seguro e produtivo.

Alinhamento com padrões de API e design de contratos

Do ponto de vista de design de APIs, status codes são parte do contrato público da sua interface. Tratá-los como constantes nomeadas reforça essa visão de contrato.

Alguns benefícios práticos:

  • Facilita a padronização de respostas entre diferentes microserviços.
  • Ajuda a manter alinhamento com guias internos de API (API Guidelines).
  • Simplifica a documentação em ferramentas como OpenAPI/Swagger, onde você pode mapear diretamente as constantes utilizadas no código para os status documentados.

Quando cada serviço expõe respostas coerentes – por exemplo, sempre usando HTTP_404_NOT_FOUND para recursos inexistentes e HTTP_422_UNPROCESSABLE_ENTITY para erros de validação –, consumidores conseguem tratar erros de forma genérica e previsível.

Integração com frameworks e bibliotecas

Em frameworks como FastAPI, é comum configurar o status code diretamente nos endpoints, muitas vezes usando constantes ou enums oriundos do próprio framework ou da biblioteca padrão de HTTP. Essa prática reduz acoplamento a valores “mágicos” e torna o código mais idiomático.

Mesmo que a stack atual não ofereça esse suporte nativamente, nada impede que você:

  • Crie um módulo helper/status_code.py com constantes alinhadas à especificação HTTP.
  • Use essas constantes tanto no backend quanto em testes automatizados, evitando duplicações.
  • Compartilhe o padrão com bibliotecas internas ou SDKs que consomem a API.

Assim, helper/status_code.py se torna um exemplo de padrão arquitetural que pode ser replicado em diferentes serviços e projetos.

Observabilidade, logs e debugging

Em ambientes de produção, a qualidade dos logs é fundamental para análise de incidentes. Status codes aparecem em traces, métricas e dashboards. Quando constantes descritivas são usadas na aplicação, é natural que o mesmo vocabulário apareça nas mensagens de log e no contexto de exceções.

Ver algo como “Falha ao chamar serviço externo: HTTP503SERVICE_UNAVAILABLE” é mais autoexplicativo do que apenas registrar “503”. Isso acelera o diagnóstico, reduz ambiguidade e diminui o tempo médio de resolução de incidentes (MTTR).

Conclusão prática para projetos Python

Para projetos Python que expõem APIs REST, adotar constantes de status code não é apenas um detalhe estético: é uma decisão de design que impacta legibilidade, manutenção, padronização e observabilidade.

  • Se o framework já fornece constantes, use-as.
  • Se não fornece, crie um módulo dedicado – como o helper/status_code.py – e estabeleça-o como padrão de equipe.
  • Garanta que todas as camadas que lidam com HTTP (handlers, services, middlewares, testes) utilizem as mesmas constantes.

Esse pequeno investimento inicial gera um retorno significativo ao longo do ciclo de vida da aplicação, tornando a base mais previsível, profissional e preparada para crescer com segurança.



Riverfount
Vicente Eduardo Ribeiro Marçal

Este artigo mostra como aplicar Abstract Base Classes (ABC) em um projeto real robusto, focado no desenvolvimento de microserviços. O objetivo é garantir clareza, contratos explícitos e extensibilidade, aliando os conceitos a práticas modernas.

Contexto do Projeto

Imagine um sistema de microserviços para gerenciamento de pedidos, em que diferentes serviços precisam manipular objetos que representam entidades diversas, como Pedido e Cliente. Queremos garantir que todas as entidades sigam um contrato explícito para operações comuns (ex.: obter ID, validação). Além disso, há um repositório genérico para armazenar dados dessas entidades com verificação de tipo.

Definição da ABC para Entidades

Criamos uma Abstract Base Class chamada Entity com métodos abstratos para garantir que toda entidade implemente os comportamentos necessários:

from abc import ABC, abstractmethod

class Entity(ABC):
    @abstractmethod
    def id(self) -> int:
        """Retorna o identificador único da entidade."""
        pass

    @abstractmethod
    def validate(self) -> bool:
        """Valida as regras de negócio da entidade."""
        pass

Implementação Concreta das Entidades

Exemplo de uma entidade Order (Pedido) que implementa a ABC e regras específicas:

class Order(Entity):
    def __init__(self, order_id: int, total: float) -> None:
        self._order_id = order_id
        self.total = total

    def id(self) -> int:
        return self._order_id

    def validate(self) -> bool:
        # Validação simples: total não pode ser negativo
        return self.total >= 0

Outro exemplo com Customer (Cliente):

class Customer(Entity):
    def __init__(self, customer_id: int, email: str) -> None:
        self._customer_id = customer_id
        self.email = email

    def id(self) -> int:
        return self._customer_id

    def validate(self) -> bool:
        # Validação simples: e-mail deve conter '@'
        return '@' in self.email

Repositório Genérico para Armazenar Entidades Validando Antes

A seguir, um repositório que aceita apenas entidades válidas, usando o tipo genérico limitado para Entity:

from typing import TypeVar, Generic, List

T = TypeVar('T', bound=Entity)

class Repository(Generic[T]):
    def __init__(self) -> None:
        self._items: List[T] = []

    def add(self, item: T) -> None:
        if not item.validate():
            raise ValueError(f"Invalid entity: {item}")
        self._items.append(item)

    def get_by_id(self, entity_id: int) -> T | None:
        for item in self._items:
            if item.id() == entity_id:
                return item
        return None

    def get_all(self) -> List[T]:
        return self._items

Uso Prático no Microserviço

def main():
    order_repo = Repository[Order]()
    customer_repo = Repository[Customer]()

    order = Order(1, 150.0)
    invalid_order = Order(2, -10.0)  # Total inválido

    customer = Customer(1, "user@example.com")
    invalid_customer = Customer(2, "invalid_email")  # E-mail inválido

    order_repo.add(order)
    try:
        order_repo.add(invalid_order)
    except ValueError as e:
        print(e)

    customer_repo.add(customer)
    try:
        customer_repo.add(invalid_customer)
    except ValueError as e:
        print(e)

    print("Pedidos:")
    for o in order_repo.get_all():
        print(f"ID: {o.id()}, Total: {o.total}")

    print("Clientes:")
    for c in customer_repo.get_all():
        print(f"ID: {c.id()}, Email: {c.email}")

if __name__ == "__main__":
    main()

Benefícios desse padrão no projeto real

  • Contratos explícitos: A ABC obriga à implementação dos métodos id e validate.

  • Segurança em tempo de execução: Objetos inválidos não serão adicionados ao repositório.

  • Reuso e manutenibilidade: O repositório é genérico e reutilizável com qualquer entidade.

  • Facilidade para testes: É simples isolar e testar entidades e repositórios separadamente.

  • Escalabilidade: Novas entidades podem ser criadas seguindo o contrato, sem mudanças na infraestrutura do repositório.

Conclusão

Esta abordagem demonstra o poder das ABCs combinadas com generics e tipagem avançada, garantindo sistemas Python mais estruturados, robustos e suscetíveis a erros minimizados, essenciais para microserviços confiáveis e manteníveis. Se desejar, posso aprofundar a integração com outros padrões ou frameworks.



Riverfount
Vicente Eduardo Ribeiro Marçal

Este artigo aborda como usar funcionalidades avançadas de tipagem em Python, como Protocols, Generics e técnicas avançadas de typing, para criar aplicações escaláveis, flexíveis e de fácil manutenção.

Protocols: Contratos Flexíveis e Estruturais

Protocols permitem definir contratos de métodos e propriedades sem herança explícita, facilitando a interoperabilidade entre microserviços. Qualquer classe que implemente os métodos definidos no protocolo pode ser usada onde esse protocolo é esperado.

Exemplo prático:

from typing import Protocol

class Serializer(Protocol):
    def serialize(self) -> bytes:
        pass

class JsonSerializer:
    def serialize(self) -> bytes:
        return b'{"user": "alice"}'

class XmlSerializer:
    def serialize(self) -> bytes:
        return b'<user>alice</user>'

def send_data(serializer: Serializer) -> None:
    data = serializer.serialize()
    print(f"Enviando dados: {data}")

send_data(JsonSerializer())
send_data(XmlSerializer())

Neste exemplo, send_data aceita qualquer objeto que implemente o método serialize, garantindo baixo acoplamento e flexibilidade.

Generics: Componentes Reutilizáveis e Tipados

Generics permitem criar classes e funções genéricas que mantêm a segurança de tipos, facilitando a modularidade.

Exemplo de repositório genérico:

from typing import TypeVar, Generic, List

T = TypeVar('T')

class Repository(Generic[T]):
    def __init__(self) -> None:
        self._items: List[T] = []

    def add(self, item: T) -> None:
        self._items.append(item)

    def get_all(self) -> List[T]:
        return self._items

class User:
    def __init__(self, username: str) -> None:
        self.username = username

user_repo = Repository[User]()
user_repo.add(User("alice"))
for user in user_repo.get_all():
    print(user.username)

Este padrão permite criar repositórios ou caches que funcionam com qualquer tipo de objeto, aumentando a reutilização e segurança de tipos.

Tipagem Avançada: Operador | e Literal

Prefira o operador | para tipos alternativos ao invés de Union e use Literal para valores fixos, reforçando contratos claros.

Exemplo:

from typing import Literal

def login(role: Literal['admin', 'user', 'guest']) -> str:
    if role == 'admin':
        return "Acesso total"
    elif role == 'user':
        return "Acesso limitado"
    return "Acesso restrito"

print(login('admin'))  # Acesso total

Isso aumenta a legibilidade e reduz riscos de erro nas chamadas de função.

Conclusão

Combinando Protocols, Generics e tipagem avançada, é possível construir aplicações com contratos claros, flexíveis e robustos, facilitando o trabalho em times desacoplados e a manutenção do código.

Essas práticas elevam a qualidade do código e tornam os sistemas mais escaláveis e confiáveis, sendo indispensáveis para desenvolvedores focados em arquiteturas modernas, principalmente as de microserviços.



Riverfount
Vicente Eduardo Ribeiro Marçal

Você já enfrentou resultados inesperados ao usar listas ou dicionários como valores padrão em funções Python? Esse é um problema comum que pode causar bugs sutis e difíceis de encontrar. Neste artigo técnico, vamos desmistificar o motivo desse comportamento, mostrando exemplos práticos e como evitá-lo com boas práticas de programação. Se você é um desenvolvedor Python buscando produzir código mais robusto e previsível, este conteúdo é essencial para o seu dia a dia.

Evitando Surpresas

Ao definir funções em Python, usar valores padrão em argumentos é comum para facilitar chamadas. Contudo, quando o valor padrão é um tipo mutável, como listas ou dicionários, isso pode causar efeitos inesperados. Vamos analisar exemplos para entender esse comportamento.

Exemplo Problemático: Lista Mutável como Valor Padrão

def add_item(item, lista=[]):
    lista.append(item)
    return lista

print(add_item('maçã'))    # Saída esperada: ['maçã']
print(add_item('banana'))  # Saída inesperada: ['maçã', 'banana']

Aqui, o segundo print adiciona o item 'banana' à mesma lista usada na primeira chamada, porque o objeto lista padrão foi criado uma vez e reutilizado. Isso acontece porque argumentos padrão são avaliados apenas na definição da função.

Corrigindo com None como Valor Padrão

A forma recomendada é usar None e criar a lista dentro da função quando necessário:

def add_item(item, lista=None):
    if lista is None:
        lista = []
    lista.append(item)
    return lista

print(add_item('maçã'))    # Saída: ['maçã']
print(add_item('banana'))  # Saída: ['banana']

Assim, cada chamada sem lista passa a criar uma nova lista vazia, evitando efeitos colaterais.

Mais um Exemplo com Dicionário Mutável

def increment_count(key, counts={}):
    counts[key] = counts.get(key, 0) + 1
    return counts

print(increment_count('python'))  # {'python': 1}
print(increment_count('java'))    # {'python': 1, 'java': 1} – resultado inesperado

No caso acima, o dicionário padrão é compartilhado e mantido entre as chamadas. A forma correta:

def increment_count(key, counts=None):
    if counts is None:
        counts = {}
    counts[key] = counts.get(key, 0) + 1
    return counts

print(increment_count('python'))  # {'python': 1}
print(increment_count('java'))    # {'java': 1}

Exemplo com Classe e Argumento Mutável

class Collector:
    def __init__(self, items=[]):
        self.items = items

    def add(self, item):
        self.items.append(item)

    def get_items(self):
        return self.items

c1 = Collector()
c1.add('foo')

c2 = Collector()
print(c2.get_items())  # Saída inesperada: ['foo']

A mesma lista é compartilhada por todas as instâncias quando passada como valor padrão mutável. Correção:

class Collector:
    def __init__(self, items=None):
        self.items = items or []

    def add(self, item):
        self.items.append(item)

    def get_items(self):
        return self.items

c1 = Collector()
c1.add('foo')

c2 = Collector()
print(c2.get_items())  # Saída correta: []

Boas Práticas para Evitar Problemas com Argumentos Mutáveis

  • Nunca use tipos mutáveis como valores padrão de argumentos. Prefira usar None e inicialize a variável dentro da função.

  • Evite efeitos colaterais em funções. Funções devem ser previsíveis e sempre retornar resultados baseados em seus argumentos.

  • Prefira tipos imutáveis para valores padrão. Imutáveis como int, str e tuple não causam esse tipo de problema.

  • Ao lidar com objetos mutáveis, clone-os quando necessário. Use métodos como copy() ou slicing para evitar alterações inesperadas.

  • Teste cuidadosamente funções que recebem parâmetros opcionais. Garanta que o estado de chamadas anteriores não afete as subsequentes.

  • Documente o comportamento esperado de seus métodos, especialmente em bibliotecas e APIs. Indique claramente se objetos mutáveis são compartilhados ou não.

Dominar essas técnicas ajuda a produzir código Python mais resiliente e menos propenso a bugs difíceis de detectar, essencial para engenheiros de software que buscam qualidade profissional em seus projetos.



Riverfount
Vicente Eduardo Ribeiro Marçal

Entenda os riscos do uso de from módulo import * em Python, saiba por que ele compromete a legibilidade e a manutenção do código e descubra as alternativas recomendadas por desenvolvedores experientes.

A armadilha da conveniência em Python

Há algo em Python que seduz até os desenvolvedores mais experientes: a promessa de simplicidade. Poucas linguagens conseguem equilibrar legibilidade e poder expressivo como ele faz. Mas é justamente essa aparente simplicidade que, às vezes, nos leva a atalhos perigosos. Entre eles, um velho conhecido: from módulo import *.

Essa linha, tão curta quanto tentadora, parece inofensiva em pequenos scripts… até que o projeto cresce, outros módulos entram em cena e o caos começa a se insinuar. O que parecia elegante se transforma em um labirinto de dependências invisíveis, nomes sobrescritos e bugs indecifráveis.

A falsa sensação de simplicidade

Importar tudo de um módulo é como abrir as portas da sua casa e deixar qualquer um entrar. De início, parece acolhedor. Mas quando algo dá errado, você não sabe quem fez a bagunça. Essa é a essência do problema: o código perde fronteiras claras.

Quando um projeto cresce, inevitavelmente surgem conflitos de nome. Uma função sua pode ter o mesmo nome de algo importado, e lá se vai a previsibilidade do comportamento do código. E o pior: o erro nem sempre se manifesta de forma imediata — ele se infiltra sutilmente, como um bug fantasma que só aparece na pior hora possível.

Legibilidade acima da brevidade

Um código é tão bom quanto sua capacidade de ser compreendido por outras pessoas (inclusive você no futuro). Ao usar import *, você apaga pistas valiosas sobre de onde vêm as funções que usa. Quando um leitor se depara com sqrt(16), ele precisa adivinhar: é uma função da biblioteca padrão, algo definido localmente ou algo importado de um módulo obscuro?

Essa incerteza sabota um princípio essencial de engenharia: previsibilidade. Códigos que exigem adivinhações são códigos menos confiáveis.

Custo oculto e impacto no código

O uso de from módulo import * não implica em maior custo de desempenho ou tempo de carregamento. Ao importar um módulo, o interpretador Python o carrega e inicializa apenas uma vez, armazenando-o em sys.modules. As importações subsequentes, com ou sem *, apenas criam novas referências aos objetos já existentes.

O verdadeiro impacto está na legibilidade e manutenção. A importação global adiciona múltiplos nomes ao namespace atual, podendo sobrescrever identificadores e dificultar a rastreabilidade das dependências. Isso reduz a previsibilidade do código e aumenta o risco de colisões de nomes e efeitos colaterais sutis, principalmente em bases de código extensas.

Em projetos de médio e grande porte, recomenda-se sempre preferir importações explícitas (import módulo ou from módulo import símbolo) para preservar a clareza e facilitar ferramentas de análise estática, refatoração e autocompletar em IDEs.

Alternativas seguras e idiomáticas em Python

A boa notícia é que a linguagem oferece opções mais claras — e alinhadas com a filosofia “Explicit is better than implicit”:

from math import sqrt, pi

Importe apenas o que você precisa e comunique intenção.

Ou, se preferir manter a origem explícita:

import math
resultado = math.sqrt(16)

A notação com ponto reforça a origem de cada função.

E em casos de bibliotecas extensas, os alias ajudam:

import pandas as pd
df = pd.read_csv("data.csv")

Essa convenção é legível, padronizada e amplamente aceita pela comunidade.

Um sinal de maturidade profissional

Código limpo não é o mais curto, e sim o mais claro. Evitar import * é um passo em direção à maturidade profissional. É escolher clareza e previsibilidade no lugar da pressa.

Em tempos em que a maioria dos bugs surge nas fronteiras entre módulos, saber exatamente de onde cada símbolo vem não é luxo — é controle.

Um convite à reflexão

Na próxima vez que você digitar from módulo import *, pause. Pergunte-se se a conveniência justifica o custo. O Python recompensa quem escolhe caminhos explícitos. E talvez o maior sinal de evolução como engenheiro Python seja perceber que clareza é o verdadeiro atalho.

🧭 Boas práticas resumidas

  • Prefira importações explícitas (from math import sqrt, pi).
  • Use alias padronizados em bibliotecas populares (import pandas as pd, import numpy as np).
  • Evite nomes genéricos que possam colidir com funções de módulos.
  • Documente qualquer alias ou importação incomum no projeto.
  • Faça revisões periódicas no código para eliminar usos antigos de import *.
  • Valorize a clareza: código previsível é sinônimo de código profissional.



Riverfount
Vicente Eduardo Ribeiro Marçal

No desenvolvimento Python, especialmente em projetos de médio a grande porte e pipelines complexos de testes automatizados, é comum encontrar erros sutis relacionados a comparações com o valor booleano True. Uma prática aparentemente inofensiva, como usar == True para verificar condições, pode introduzir comportamentos inesperados que dificultam a manutenção, geram falsos positivos em testes e causam dúvidas em revisões de código.

Essas situações não são incomuns em equipes que lidam com múltiplas camadas de abstração — desde o código de negócio até frameworks de teste — e evidenciam a importância de entender profundamente a diferença entre identidade e igualdade em Python, bem como as melhores práticas para escrever condicionais claras e robustas.

Entendendo o impacto de is True vs == True

O operador is verifica se dois objetos são exatamente os mesmos na memória — ou seja, se têm identidade. No caso de var is True, o teste passa somente se var for exatamente o objeto singleton True do Python.

Já o operador == compara valores, permitindo que diversos objetos considerados “truthy” no contexto booleano, como 1, Strings não vazias ou listas, sejam equivalentes a True quando comparados com var == True. Isso pode causar falhas silenciosas ou testes que passam indevidamente.

if 1 == True:  # Avalia para True, embora 1 não seja o objeto True
    print("Isso pode confundir a lógica.")

Esse tipo de resultado pode mascarar bugs ou comportamentos inesperados, especialmente em testes unitários.

Evitando comparações explícitas desnecessárias

O consenso na comunidade Python — refletido em PEP8 e amplamente adotado — é que a maioria das comparações explícitas com True e False são redundantes e prejudicam a legibilidade. Python permite confiar diretamente na avaliação booleana implícita, que é mais limpa e expressiva.

# Menos legível
if var == True:
    ...

# Idiomático
if var:
    ...

# Para casos False
if not var:
    ...

Essa abordagem reduz o ruído visual e elimina ambiguidades causadas por diferentes tipos que avaliam para verdadeiro ou falso.

Exemplos práticos de erros comuns

  • Erro em comparações com listas booleanas:
a = [True, True, False]
b = [True, True, True]

result = a and b  
print(result)  # Saída: [True, True, True]

Aqui, usar and entre listas não faz uma comparação elemento a elemento, mas retorna o último valor avaliado. Isso pode levar a resultados inesperados, especialmente se se esperava um valor booleano.

  • Confusão com tipos de retornos em bibliotecas como NumPy:

Em NumPy, comparações booleanas não retornam True ou False do Python nativo, mas tipos como np.True_ ou np.False_, que podem quebrar testes que usam is True:

import numpy as np

result = np.array([1, 2, 3]) == 1
print(result)          # array([ True, False, False])
print(result[0] is True)  # False, pois é np.bool_, não bool nativo

Essa sutileza é fonte comum de bugs em projetos científicos e de análise de dados.

  • Falsos positivos em testes unitários:
def test_func():
    result = 1
    assert result == True  # Passa, mas result não é booleano

    assert result is True  # Falha, aqui a precisão salva o teste

O uso de is True impede que valores “truthy” como 1 passem em testes que esperam tipos booleanos.

Casos excepcionais: validação rigorosa em testes unitários

Há, todavia, situações específicas — especialmente em testes unitários — onde a precisão semântica é necessária. Quando uma função ou método deve retornar explicitamente o valor booleano True, e não apenas um “valor truthy” qualquer, o uso de assert ... is True garante que o teste falhe se houver qualquer discrepância de tipo ou valor.

def is_even(number):
    return number % 2 == 0

def test_is_even():
    result = is_even(4)

    # Verificação rigorosa: valida identidade
    assert result is True

    # Evitar
    assert result == True

Essa prática ajuda a prevenir falsos positivos em pipelines de CI/CD, assegurando que o comportamento funcional esperado esteja alinhado com tipos e valores corretos e não somente com avaliações booleanas superficiais.

Conclusão: clareza e rigor em equilíbrio

Escrever código Python robusto é também dominar as sutilezas da linguagem. Evitar comparações explícitas desnecessárias com True não só melhora a clareza, como também reduz riscos de bugs ocultos.

Por outro lado, saber quando estabelecer verificações estritas, como em testes unitários com is True, demonstra maturidade técnica e compromisso com a qualidade.

Equilibrar legibilidade e precisão é o passo decisivo que diferencia código “funcional” de código profissional, limpo e confiável.



Riverfount
Vicente Eduardo Ribeiro Marçal

Resumo:
O Princípio da Inversão de Dependência (DIP), parte do conjunto SOLID, é fundamental para criar sistemas sustentáveis, extensíveis e fáceis de testar. Este artigo explora como aplicá-lo em Python usando typing.Protocol e injeção de dependência, com foco em arquiteturas limpas e aplicação prática em sistemas corporativos.

Contexto

Projetos orientados a objetos de longo prazo exigem mais do que modularidade: precisam de estabilidade arquitetural. O Princípio da Inversão de Dependência (Dependency Inversion Principle – DIP) aborda exatamente esse ponto.
Ele recomenda que módulos de alto nível (os que contêm as regras de negócio) não conheçam os detalhes de baixo nível (implementações, drivers, frameworks), mas interajam por meio de abstrações.

Mesmo em uma linguagem dinâmica como Python, onde acoplamentos podem parecer menos problemáticos, o DIP se torna essencial em sistemas corporativos com múltiplos serviços e integrações externas, garantindo desacoplamento e testabilidade.

O problema: quando o código depende de detalhes

Imagine um serviço que envia notificações a usuários. Uma implementação comum é instanciar dependências diretamente dentro da classe de negócio:

class EmailService:
    def send_email(self, to: str, message: str) -> None:
        print(f"Enviando e-mail para {to}: {message}")


class UserNotifier:
    def __init__(self) -> None:
        self.email_service = EmailService()  # dependência concreta

    def notify_user(self, user_email: str, msg: str) -> None:
        self.email_service.send_email(user_email, msg)

Embora funcional, essa abordagem cria acoplamento rígido. Qualquer mudança no método de envio (ex.: SMS, Push, Webhook) exige alterar UserNotifier, o que viola diretamente o DIP e propaga dependências desnecessárias.

A solução: abstrações com Protocols

O DIP recomenda inverter essa dependência — o módulo de alto nível deve depender de uma abstração, e não de um detalhe concreto.
Desde o Python 3.8, a PEP 544 introduziu typing.Protocol, permitindo descrever contratos de interface de modo estático e seguro.

from typing import Protocol


class Notifier(Protocol):
    def send(self, to: str, message: str) -> None:
        ...

A partir do contrato, diferentes mecanismos podem ser implementados:

class EmailNotifier:
    def send(self, to: str, message: str) -> None:
        print(f"Email para {to}: {message}")


class SMSNotifier:
    def send(self, to: str, message: str) -> None:
        print(f"SMS enviado para {to}: {message}")

Assim, o módulo de negócio depende apenas de uma abstração genérica:

class UserNotifier:
    def __init__(self, notifier: Notifier) -> None:
        self._notifier = notifier

    def notify(self, user_email: str, msg: str) -> None:
        self._notifier.send(user_email, msg)

O uso torna-se desacoplado e configurável:

email_notifier = EmailNotifier()
user_notifier = UserNotifier(email_notifier)
user_notifier.notify("joao@example.com", "Bem-vindo ao sistema!")

sms_notifier = SMSNotifier()
user_notifier = UserNotifier(sms_notifier)
user_notifier.notify("+5511999999999", "Código de autenticação: 123456")

Benefícios e impacto arquitetural

A aplicação do DIP resulta em ganhos tangíveis de engenharia:

  • Desacoplamento estrutural: classes de domínio não conhecem implementações concretas.
  • Extensibilidade controlada: adicionar novos canais ou comportamentos não requer refatoração de código existente.
  • Testabilidade facilitada: dependências podem ser simuladas ou injetadas em testes unitários.
  • Conformidade com arquiteturas limpas: o domínio permanece independente da infraestrutura.

Em projetos complexos, contêineres de injeção como dependency-injector ou punq podem automatizar a resolução de dependências sem comprometer a clareza arquitetural.

Boas práticas e armadilhas comuns

Boas práticas

  • Defina contratos explícitos: sempre que um módulo precisar interagir com outro de baixo nível, defina um Protocol.
  • Mantenha o domínio puro: o código de negócio deve ser independente de frameworks e bibliotecas externas.
  • Use tipagem estática: ferramentas como mypy ajudam a validar conformidade de implementações com Protocols.
  • Aplique injeção de dependência: crie instâncias fora do domínio e injete-as no construtor (ou em fábricas específicas).

Armadilhas frequentes

  • Overengineering: evite criar abstrações desnecessárias. Se há apenas uma implementação e não há expectativa de variação, o custo de manter o contrato pode não compensar.
  • Dependência indireta: trocar dependência direta por uma indireta mal desenhada (por exemplo, uma abstração genérica demais) reduz a clareza do sistema.
  • Confusão entre abstração e herança: Protocols substituem interfaces, não exigem herança e não impõem rigidez hierárquica.

Adotar o DIP não significa adicionar camadas de complexidade artificial, mas desenhar fronteiras claras entre políticas e detalhes técnicos.

Conclusão

O Princípio da Inversão de Dependência é mais do que uma regra teórica do SOLID: é uma mentalidade de design voltada à estabilidade e evolução contínua.
Em Python, o uso de Protocol e injeção de dependência permite aplicar o DIP de forma idiomática, preservando a simplicidade da linguagem sem abrir mão da qualidade arquitetural.
Em sistemas que precisam evoluir com segurança, o DIP é uma das práticas mais valiosas — e um dos marcos de maturidade de um engenheiro de software sênior.



Riverfount
Vicente Eduardo Ribeiro Marçal

O Princípio da Segregação de Interfaces (ISP — Interface Segregation Principle) é um dos pilares do SOLID e trata diretamente da qualidade dos contratos entre componentes. Em essência, ele afirma que uma classe não deve ser obrigada a depender de métodos que não utiliza. Essa regra incentiva o desenho de interfaces menores, mais coesas e representativas de um papel específico no sistema.

Na prática, o ISP força uma reflexão arquitetural: qual é a verdadeira responsabilidade dessa abstração? Se a resposta envolve comportamentos heterogêneos, a interface provavelmente está concentrando demasiadas responsabilidades — um sinal de design frágil e baixo reuso.

O problema das interfaces genéricas

Considere um caso comum: um módulo que define uma interface genérica de “dispositivo multifuncional”. Ela impõe à hierarquia de classes um contrato extenso, mesmo que nem todas as implementações precisem de todas as operações.

from abc import ABC, abstractmethod

class MultiFunctionDevice(ABC):
    @abstractmethod
    def print_document(self, document): pass

    @abstractmethod
    def scan_document(self, document): pass

    @abstractmethod
    def fax_document(self, document): pass


class BasicPrinter(MultiFunctionDevice):
    def print_document(self, document):
        print(f"Imprimindo: {document}")

    def scan_document(self, document):
        raise NotImplementedError("Este dispositivo não suporta digitalização")

    def fax_document(self, document):
        raise NotImplementedError("Este dispositivo não envia fax")

Aqui, BasicPrinter viola o ISP porque é forçada a implementar métodos irrelevantes. Qualquer alteração em MultiFunctionDevice pode afetar classes que não deveriam ter relação entre si.

Refinando o design com interfaces específicas

Para evitar esse problema, segmentamos as interfaces em abstrações menores e mais focadas:

from abc import ABC, abstractmethod

class Printable(ABC):
    @abstractmethod
    def print_document(self, document): pass

class Scannable(ABC):
    @abstractmethod
    def scan_document(self, document): pass

class Faxable(ABC):
    @abstractmethod
    def fax_document(self, document): pass


class BasicPrinter(Printable):
    def print_document(self, document):
        print(f"Imprimindo: {document}")


class MultiFunctionPrinter(Printable, Scannable, Faxable):
    def print_document(self, document):
        print(f"Imprimindo: {document}")

    def scan_document(self, document):
        print(f"Digitalizando: {document}")

    def fax_document(self, document):
        print(f"Enviando fax: {document}")

Cada interface é coesa e independente. As classes agora implementam apenas as operações relevantes às suas funcionalidades, reduzindo o acoplamento e melhorando a clareza estrutural.

Abordagem moderna com typing.Protocol

A partir do Python 3.8, typing.Protocol permite expressar contratos comportamentais baseados em tipagem estrutural (também chamada de duck typing verificado estaticamente). Esse recurso é especialmente compatível com o ISP, pois elimina a necessidade de herança explícita para validar conformidade de tipo.

from typing import Protocol

class Printable(Protocol):
    def print_document(self, document: str) -> None: ...

class Scannable(Protocol):
    def scan_document(self, document: str) -> None: ...


class BasicPrinter:
    def print_document(self, document: str) -> None:
        print(f"Imprimindo: {document}")


class SmartDevice:
    def print_document(self, document: str) -> None:
        print(f"Imprimindo: {document}")

    def scan_document(self, document: str) -> None:
        print(f"Digitalizando: {document}")


def print_any(printer: Printable, content: str) -> None:
    printer.print_document(content)

Observe que BasicPrinter e SmartDevice não herdam explicitamente de Printable ou Scannable, mas o type checker reconhecerá ambas as classes como compatíveis por possuírem os métodos exigidos. Essa abordagem é vantajosa porque:

  • Mantém baixo acoplamento entre tipos, reforçando a aplicação do ISP.
  • Usa duck typing com suporte de tipagem estática (útil em ferramentas como mypy).
  • Favorece design evolutivo; novos comportamentos podem ser adicionados a outras classes sem quebrar a hierarquia.

Assim, Protocol é a forma moderna e idiomática de aplicar o ISP em projetos Python, tornando clara a separação de responsabilidades e preservando a flexibilidade da linguagem.

Conclusão

O ISP é mais que um princípio de design: é uma diretriz para compor sistemas orientados a abstrações coesas e independentes. No ecossistema Python, a evolução da tipagem com abc e Protocol oferece duas formas complementares de expressar esse princípio — uma baseada em herança nominal, outra em compatibilidade estrutural.

Projetar interfaces enxutas e especializadas é um ato de disciplina arquitetural: reduz o impacto de mudanças, aumenta a clareza e favorece a manutenibilidade a longo prazo. Em times maduros, a aplicação do ISP reflete um domínio avançado de separação de responsabilidades e uma compreensão profunda da dinâmica entre contrato e implementação.



Riverfount
Vicente Eduardo Ribeiro Marçal