Valores monetários, impostos, taxas, medições e regras contábeis exigem resultados previsíveis. O tipo float é rápido e adequado para muitas tarefas científicas, mas representa números em base binária. Por isso, frações decimais simples como 0,1 nem sempre são armazenadas exatamente. O módulo Decimal no Python oferece aritmética decimal com precisão configurável, regras explícitas de arredondamento e mecanismos para detectar operações inexatas.
Neste guia, você aprenderá a criar valores corretamente, usar quantize(), configurar contextos, escolher modos de arredondamento, ativar traps, validar entradas e integrar Decimal com APIs e bancos de dados. O conteúdo complementa nossos artigos sobre float em Python, f-strings para números e moedas, tipos de dados, UUIDs e cópias de objetos.
Por que float surpreende
Em ponto flutuante binário, vários valores decimais são aproximações:
print(0.1 + 0.2) # 0.30000000000000004
print(0.1 + 0.1 + 0.1 == 0.3) # FalseIsso não é um defeito específico do Python. A documentação oficial sobre ponto flutuante explica que 0,1 possui expansão infinita em base 2, assim como 1/3 possui expansão infinita em base 10. O computador armazena a fração binária representável mais próxima.
Para simulações e gráficos, o erro costuma ser aceitável. Para contabilidade, preços e invariantes de igualdade decimal, uma representação em base 10 é frequentemente mais apropriada.
Criando Decimal a partir de strings
Importe Decimal e passe uma string com o valor pretendido:
from decimal import Decimal
preco = Decimal("19.90")
taxa = Decimal("0.075")
total = preco * (Decimal("1") + taxa)
print(total)A string preserva exatamente os dígitos informados, inclusive zeros finais significativos. Para valores vindos de formulários, JSON textual ou configurações, essa é a forma preferida.
Evite Decimal construído de float
Construir diretamente com um float converte exatamente a aproximação binária já armazenada:
from decimal import Decimal
print(Decimal(0.1))
# 0.100000000000000005551115123125782...Isso pode ser útil para inspecionar o valor real de um float, mas não para representar o decimal pretendido pelo usuário. Prefira Decimal("0.1"). Quando uma biblioteca entrega float e você precisa apenas da representação exibida, uma estratégia deliberada é converter por str(), documentando a perda controlada:
valor = Decimal(str(0.1))Inteiros e Decimal.from_number()
Inteiros são convertidos exatamente:
quantidade = Decimal(3)
subtotal = Decimal("9.90") * quantidadeNo Python 3.14, Decimal.from_number() aceita int, float ou outro Decimal, mas não strings. Um float continua sendo convertido com todos os seus dígitos binários equivalentes.
valor = Decimal.from_number(314)
print(valor) # 314Significância e zeros finais
Decimal conserva zeros finais para indicar significância:
from decimal import Decimal
print(Decimal("1.30") + Decimal("1.20")) # 2.50
print(Decimal("1.30") * Decimal("1.20")) # 1.5600O valor numérico de 2,50 é igual ao de 2,5, mas a representação pode comunicar duas casas decimais. Essa característica é útil em relatórios financeiros e medições.
Arredondando com quantize()
quantize() arredonda para o expoente de outro Decimal. Para duas casas:
from decimal import Decimal, ROUND_HALF_UP
CENTAVO = Decimal("0.01")
valor = Decimal("7.325")
resultado = valor.quantize(CENTAVO, rounding=ROUND_HALF_UP)
print(resultado) # 7.33Use uma constante para evitar repetir a escala. O arredondamento deve acontecer no ponto definido pela regra de negócio: por item, por linha, por imposto ou apenas no total. Arredondar em etapas diferentes pode produzir resultados diferentes.
ROUND_HALF_EVEN e ROUND_HALF_UP
O contexto padrão usa ROUND_HALF_EVEN, também conhecido como arredondamento para o par. Em empates exatos, escolhe o último dígito par e reduz viés acumulado.
from decimal import Decimal, ROUND_HALF_EVEN, ROUND_HALF_UP
valor = Decimal("2.5")
print(valor.quantize(Decimal("1"), rounding=ROUND_HALF_EVEN)) # 2
print(valor.quantize(Decimal("1"), rounding=ROUND_HALF_UP)) # 3Muitas regras comerciais exigem ROUND_HALF_UP. Outras usam truncamento, teto ou piso. Não escolha pelo nome mais familiar; siga a legislação ou especificação do domínio.
Outros modos de arredondamento
O módulo oferece ROUND_DOWN, ROUND_UP, ROUND_FLOOR, ROUND_CEILING, ROUND_HALF_DOWN e ROUND_05UP. A diferença entre down e floor aparece em valores negativos: ROUND_DOWN aproxima de zero, enquanto ROUND_FLOOR aproxima de menos infinito.
Escreva testes com números positivos, negativos e empates. Um conjunto que testa apenas 1,235 pode esconder erros em -1,235.
O contexto decimal
A documentação oficial de decimal organiza o módulo em números, contextos e sinais. O contexto define precisão, arredondamento, limites de expoente, flags e traps.
from decimal import Decimal, getcontext
contexto = getcontext()
print(contexto.prec) # 28 por padrão
print(contexto.rounding)
contexto.prec = 10
print(Decimal(1) / Decimal(7))A precisão controla o número de dígitos significativos das operações, não quantas casas decimais o resultado terá. A criação a partir de string conserva todos os dígitos; o contexto entra em ação durante cálculos.
Use localcontext() para mudanças temporárias
Alterar o contexto global no meio de uma biblioteca pode surpreender outros códigos. Use localcontext() para isolar a configuração:
from decimal import Decimal, localcontext
with localcontext(prec=50) as ctx:
resultado = Decimal(1) / Decimal(7)
print(resultado)
# contexto anterior restauradoEsse padrão é ideal para uma etapa de alta precisão sem alterar o restante da aplicação. Em Python 3.11 ou posterior, atributos podem ser informados diretamente como argumentos nomeados.
Flags e sinais
Operações podem sinalizar condições como Inexact, Rounded, DivisionByZero, InvalidOperation, Overflow e Underflow. As flags ficam marcadas até serem limpas.
from decimal import Decimal, getcontext
ctx = getcontext()
ctx.clear_flags()
Decimal(1) / Decimal(7)
print(ctx.flags)Flags permitem auditar um lote sem interrompê-lo. Limpe-as antes da etapa monitorada, execute os cálculos e inspecione as condições no final.
Traps transformam sinais em exceções
Uma trap ativa faz o sinal lançar exceção. Isso é útil quando a aplicação não pode aceitar arredondamento silencioso.
from decimal import Decimal, Inexact, localcontext
with localcontext() as ctx:
ctx.traps[Inexact] = True
try:
Decimal("1") / Decimal("3")
except Inexact:
print("Resultado inexato não permitido")Você pode usar traps em validações financeiras, importações e testes. Em produção, capture exceções específicas e apresente mensagens adequadas, sem esconder dados inconsistentes.
Detectando mistura com float
O sinal FloatOperation ajuda a impedir que floats entrem acidentalmente no fluxo decimal.
from decimal import Decimal, FloatOperation, localcontext
with localcontext() as ctx:
ctx.traps[FloatOperation] = True
Decimal(3.14) # levanta FloatOperationEssa proteção é valiosa em módulos financeiros. Ela força a equipe a decidir conscientemente como converter cada fonte.
Exemplo de preço, desconto e imposto
from decimal import Decimal, ROUND_HALF_UP
CENTAVO = Decimal("0.01")
preco = Decimal("149.90")
quantidade = Decimal("3")
desconto = Decimal("0.10")
imposto = Decimal("0.075")
subtotal = preco * quantidade
apos_desconto = subtotal * (Decimal("1") - desconto)
total = apos_desconto * (Decimal("1") + imposto)
total = total.quantize(CENTAVO, rounding=ROUND_HALF_UP)
print(total)Em uma aplicação real, confirme se desconto e imposto são calculados por item ou sobre o total. A biblioteca executa a regra; ela não escolhe a regra contábil.
Percentuais e taxas
Converta “7,5%” para Decimal("0.075"). Evite dividir um float por 100 antes da conversão. Para entrada textual localizada, normalize separadores com uma função explícita, rejeitando formatos ambíguos.
def percentual(texto: str) -> Decimal:
valor = Decimal(texto.replace(",", "."))
return valor / Decimal("100")Essa função simples não deve aceitar milhares e moedas sem uma biblioteca de localização. Uma string como “1.234,56” exige regras de locale e validação mais rigorosa.
Comparações e tipos mistos
Somar Decimal e float gera TypeError, o que evita mistura silenciosa:
Decimal("1.2") + 1.2 # TypeErrorInteiros podem participar normalmente. Comparações com floats são suportadas, mas podem refletir a aproximação binária. Em um domínio decimal, normalize os dois operandos antes de comparar.
NaN, Infinity e zero negativo
Decimal suporta valores especiais:
from decimal import Decimal
valores = [
Decimal("NaN"),
Decimal("Infinity"),
Decimal("-Infinity"),
Decimal("-0"),
]Não permita que esses valores entrem em preços e saldos sem uma política explícita. Use is_finite(), is_nan() e is_infinite() na validação.
Serialização em JSON
O módulo json padrão não serializa Decimal automaticamente. Converter para float reintroduz aproximação. Para APIs financeiras, envie string:
registro = {
"total": str(Decimal("19.90")),
"currency": "BRL",
}Documente o contrato para que o consumidor interprete a string como decimal. Outra opção é usar um encoder ou framework que preserve o tipo.
Banco de dados
Use colunas DECIMAL ou NUMERIC com precisão e escala definidas. Não armazene dinheiro em coluna binária FLOAT. ORMs normalmente convertem esses campos para Decimal.
Valide os limites antes da gravação. Um campo NUMERIC(10,2) não aceita qualquer magnitude, e o banco pode arredondar ou rejeitar conforme a configuração.
Desempenho
Decimal costuma ser mais lento que float porque oferece regras e precisão adicionais. Não substitua todos os floats automaticamente. Use float para gráficos, física e cálculos numéricos tolerantes; Decimal para valores decimais exatos e arredondamento regulamentado.
Meça pipelines grandes. Às vezes, armazenar centavos como inteiros é mais simples, desde que a moeda e a escala sejam fixas. Juros, câmbio e quantidades fracionárias frequentemente exigem Decimal.
Testando cálculos decimais
Testes devem cobrir empates de arredondamento, números negativos, limites, entradas inválidas e ordem das operações.
from decimal import Decimal, ROUND_HALF_UP
assert Decimal("2.675").quantize(
Decimal("0.01"), rounding=ROUND_HALF_UP
) == Decimal("2.68")Compare Decimal com Decimal, não com float. Também teste invariantes: total das parcelas, soma de impostos e reconciliação com o banco.
Erros frequentes
- Criar
Decimal(0.1)esperando exatamente 0,1. - Arredondar apenas na apresentação quando a regra exige arredondamento contábil.
- Modificar o contexto global dentro de uma função reutilizável.
- Misturar Decimal e float.
- Converter para float antes de serializar.
- Escolher o modo de arredondamento sem consultar a regra do domínio.
- Ignorar flags de inexatidão.
- Usar Decimal em todo cálculo sem medir desempenho.
Boas práticas
- Crie valores de strings ou inteiros.
- Centralize constantes como centavo e modos de arredondamento.
- Use
localcontext()para precisão temporária. - Ative
FloatOperationem módulos críticos. - Valide valores finitos.
- Armazene em tipos DECIMAL/NUMERIC.
- Serializar como string quando a precisão deve ser preservada.
- Teste regras financeiras com exemplos oficiais.
Conclusão
O módulo Decimal no Python oferece controle que o ponto flutuante binário não foi projetado para fornecer: representação decimal exata, zeros significativos, precisão configurável, regras de arredondamento e sinais auditáveis.
O uso correto começa na entrada: crie valores a partir de strings, defina a escala com quantize(), isole contextos e impeça misturas acidentais com float. Quando regras de negócio e armazenamento seguem o mesmo contrato, cálculos de dinheiro, taxas e medições tornam-se reproduzíveis, testáveis e muito menos sujeitos a diferenças de centavos.







