Skip to content

Navigation Menu

Sign in
Appearance settings

Search code, repositories, users, issues, pull requests...

Provide feedback

We read every piece of feedback, and take your input very seriously.

Saved searches

Use saved searches to filter your results more quickly

Sign up
Appearance settings

RFC: Estratégia para tratamento de valores inválidos nas funções do brazilian-utils #671

Discussion options

Contexto

Hoje, várias funções do brutils retornam None quando recebem valores inválidos.
Alguns exemplos (extraídos da documentação):

  • format_cpf(...) → retorna str ou None se o CPF é inválido.
  • format_cnpj(...) → retorna str ou None se o CNPJ é inválido.
  • format_cep(...) → retorna str ou None se o CEP é inválido.
  • format_phone(...) → retorna str ou None se o telefone é inválido.
  • Diversas outras funções de formatação e conversão seguem esse padrão.

Objetivo

  • Definir uma estratégia padronizada para tratamento de valores inválidos nas funções do brazilian-utils.
  • Preferencialmente, alinhar com a prática Python de levantar exceções quando o input é inválido.
  • Fazer isso de forma:
    • bem documentada,
    • previsível para quem usa a lib,
    • com um caminho de migração suave e incremental,
    • sem quebrar código existente até uma major release.

Proposta de abordagem

Estratégia: Strict Mode Global + Parâmetro strict por função + Decorador @strictify

Esta abordagem combina três níveis de controle:

  1. Strict mode global — ativa/desativa o comportamento estrito para toda a lib
  2. Parâmetro strict por função — permite controle granular por chamada
  3. Decorador @strictify — centraliza a lógica de decisão e permite migração incremental

Prioridade de comportamento

O decorador @strictify decide se deve levantar exceção seguindo esta ordem:

  1. Se strict=True foi passado na função → levanta exceção
  2. Se strict=False foi passado na função → retorna None (comportamento legado)
  3. Se strict não foi passado, verifica o strict mode global:
    • Se strict_mode_enabled()levanta exceção
    • Caso contrário → retorna None (comportamento legado)

Componentes da solução

1. Hierarquia de exceções customizadas

class BrutilsError(Exception):
 """Exceção base para todos os erros do brutils."""
 pass
class InvalidCPFError(BrutilsError):
 """Levantada quando um CPF inválido é fornecido."""
 pass
class InvalidCNPJError(BrutilsError):
 """Levantada quando um CNPJ inválido é fornecido."""
 pass
class InvalidCEPError(BrutilsError):
 """Levantada quando um CEP inválido é fornecido."""
 pass
# ... outras exceções específicas

2. Strict mode global

_STRICT_MODE = False
def enable_strict_mode() -> None:
 """Ativa o strict mode globalmente para todas as funções do brutils."""
 global _STRICT_MODE
 _STRICT_MODE = True
def disable_strict_mode() -> None:
 """Desativa o strict mode globalmente."""
 global _STRICT_MODE
 _STRICT_MODE = False
def strict_mode_enabled() -> bool:
 """Retorna True se o strict mode está ativo globalmente."""
 return _STRICT_MODE

3. Decorador @strictify

def strictify(func):
 def wrapper(*args, **kwargs):
 try:
 return func(*args, **kwargs)
 except Exception as e:
 if strict_mode_enabled() or kwargs.get("strict"):
 raise # novo comportamento
 return None # comportamento legado
 return wrapper

4. Adicionar warnings sobre futura mudança de default

import warnings
def format_cpf(cpf: str, *, strict: bool | None = None) -> str:
 if strict is None and not strict_mode_enabled():
 warnings.warn(
 "O comportamento padrão de format_cpf mudará na v3.0.0. "
 "CPFs inválidos levantarão InvalidCPFError ao invés de retornar None. "
 "Use strict=False para manter o comportamento atual.",
 DeprecationWarning,
 stacklevel=2
 )
 # ... resto da função

Resultado: Usuários são avisados com antecedência sobre a mudança futura.

Vantagens desta abordagem

Não força usuários atuais a mudar nada — comportamento legado preservado até v3.0.0

Permite evolução incremental da lib — migrar 1 função por vez, sem pressa

Introduz comportamento mais seguro sem atrito — usuários podem adotar quando quiserem

Granularidade total — controle global E por função

Alinhado com práticas de grandes libs Python — Django, Pydantic, SQLAlchemy, Requests, Boto3 usam estratégias similares

Facilita testes e code review — mudanças pequenas e incrementais

Tipagem estática melhorada — funções em strict mode podem retornar str ao invés de str | None

Tarefas de implementação

Fase 1: Infraestrutura

  • Criar módulo brutils/exceptions.py com hierarquia de exceções
  • Implementar strict mode global em brutils/__init__.py
  • Implementar decorador @strictify
  • Adicionar testes para infraestrutura
  • Documentar no README.md e README_EN.md

Fase 2: Migração incremental

  • Migrar format_cpf e is_valid_cpf
  • Migrar format_cnpj e is_valid_cnpj
  • Migrar format_cep e is_valid_cep
  • Migrar demais utilitários

Alternativas Consideradas

Usar ValueError padrão ao invés de exceções customizadas

Descrição:

Ao invés de criar uma hierarquia de exceções customizadas (BRUtilsError, InvalidCPFError, etc.), usar apenas ValueError do Python padrão.

Exemplo:

@strictify
def format_cpf(cpf: str, *, strict: bool | None = None) -> str:
 """
 Formata um CPF válido.

 Raises:
 ValueError: Se o CPF é inválido e strict mode está ativo
 """
 if not is_valid_cpf(cpf):
 raise ValueError(f"Invalid CPF: {cpf}")
 
 return cpf_formatado

Perguntas para discussão

  1. Vocês concordam com essa abordagem de migração incremental + strict mode global + parâmetro granular?

  2. A ordem de prioridade (parâmetro strict > modo global > legado) faz sentido?

  3. Os nomes escolhidos estão adequados?

    • enable_strict_mode() / disable_strict_mode() / strict_mode_enabled()
    • Parâmetro strict (ao invés de raise_exceptions)
    • Decorador @strictify
    • Exceções como InvalidCPFError, InvalidCNPJError, etc.
  4. Existe alguma função específica onde esse modelo não se aplica bem?

Note

Comentários, contrapontos e sugestões são super bem-vindos!

You must be logged in to vote

Replies: 2 comments

Comment options

@niltonpimentel02 ficou muito bom o RFC! Super claro, bem estruturado e fácil de acompanhar.

Quem sabe, vale até a gente até criar uma categoria específica só de RFC nas Discussions para centralizar esse tipo de proposta, seria ótimo pra manter histórico e abrir espaço pra mais ideias assim.

Sobre os pontos técnicos:

  1. O único ajuste seria na lógica do decorator, porque acredito que a implementação atual não reflete exatamente a prioridade descrita no próprio RFC. Hoje ele trata só dois cenários:
except Exception:
 if strict_mode_enabled() or kwargs.get("strict"):
 raise
 return None

Isso faz com que strict=False acabe ignorado caso o strict mode global esteja ativado. Para refletir certim, pensei em algo nessa linha:

def strictify(func):
 def wrapper(*args, **kwargs):
 strict_flag = kwargs.get("strict", None)
 try:
 return func(*args, **kwargs)
 except Exception:
 if strict_flag is True:
 raise
 if strict_flag is False:
 return None
 if strict_mode_enabled():
 raise
 return None
 return wrapper
  1. Outra ideia: talvez possamos mover a lógica de warnings para dentro do próprio decorator, pra evitar repetir isso em cada função. Algo como:
def strictify(func):
 def wrapper(*args, **kwargs):
 strict_flag = kwargs.get("strict", None)
 try:
 return func(*args, **kwargs)
 except Exception as e:
 # strict=True → sempre levanta
 if strict_flag is True:
 raise
 # strict=False → comportamento legado
 if strict_flag is False:
 return None
 # strict não passado → verifica modo global
 if strict_mode_enabled():
 raise
 # comportamento legado → avisar que isso mudará
 warnings.warn(
 (
 f"O comportamento padrão de {func.__name__} mudará a partir da versão 3.x."
 f"Valores inválidos passarão a levantar {type(e).__name__} "
 "ao invés de retornar None. "
 "Use strict=False para manter o comportamento atual."
 ),
 DeprecationWarning,
 stacklevel=2
 )
 return None
 return wrapper

Assim a gente mantém a mensagem informativa, clara e bem parecida com a original, mas de forma genérica e concentrada dentro do decorator, sem precisar repetir isso em todas as funções, e só dando o warning para quem não migrou ainda.

Na sugestão, também mudei a parte do texto do warning que dizia mudará na v3.0.0. para mudará a partir da versão 3.x. - para deixar mais claro e mais amplo.


E queria também ouvir a opinião do @hyanmandian, como vocês estão lidando com isso na versão TS? Será que faria sentido algo nessa linha por lá também? Mesmo que ajustado ao ecossistema, poderia ser interessante manter uma coerência conceitual entre as libs.

Mas no geral, para mim tá sucesso demais, só dale!

Valeu!!

You must be logged in to vote
0 replies
Comment options

Primeiramente, parabéns pela RFC @niltonpimentel02, ficou muito boa!

Dando os meus 2 centavos aqui na discussão, gostei da solução proposta. Para ser sincero eu fiquei com um sentimento dúbio. Por um lado achei uma solução bastante completa, por outro achei que possui um grau de complexidade que eu não vi na biblioteca como um todo. Refletindo sobre essa questão eu tendo mais a ser a favor de adicionarmos um sistema mais robusto, mesmo que torne a biblioteca mais complexa. Acho que a tendência é esta biblioteca se tornar cada vez mais robusta e, logo, adicionar complexidade se torna inevitável. Gostei da solução final como foi colocada pela @camilamaia e, para ser sincero, acho que vocês estão muito mais por dentro dos patterns atuais do Python do que eu. Vou tentar contribuir com esta implementação, mas acho que vou mais seguir vocês.

Sobre as perguntas colocas pelo @niltonpimentel02:
1 - Sim, concordo.
2 - Sim, faz sentido.
3 - Sobre os nomes das funções, eu alteraria a função strict_mode_enabled para strict_mode_is_enabled para tentar fugir um pouco da semelhança com a função enable_strict_mode. De resto, tudo OK.

You must be logged in to vote
0 replies
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
RFC & ADR Request for Comments e Architecture Decision Record

AltStyle によって変換されたページ (->オリジナル) /