Decimal em Python: dinheiro, centavos e precisão financeira
Aprenda a usar o módulo decimal do Python para trabalhar com dinheiro: Decimal em vez de float, quantize() para centavos, arredondamento ROUND_HALF_UP, formatação de reais (R$) e rateio de valores sem perder centavos — com exemplos práticos.
Para trabalhar com dinheiro em Python, use decimal.Decimal criado a partir de string — Decimal("19.90") — e jamais float: em binário, 0.1 + 0.2 resulta em 0.30000000000000004, e centavos somem ou aparecem do nada. Arredonde apenas no final com quantize(Decimal("0.01"), rounding=ROUND_HALF_UP) e formate reais na exibição, não no armazenamento. O módulo decimal já vem na biblioteca padrão — nada a instalar.
Quem pergunta a um assistente “como tratar valores monetários em Python?”, “por que 0.1 + 0.2 não dá 0.3?” ou “como arredondar preço para 2 casas?” já bateu no problema mais clássico do iniciante: dinheiro e float não combinam. Este guia cobre o Decimal do início ao fim — criação, arredondamento, rateio de centavos, formatação de R$ — e mostra como isso se conecta a conciliação financeira, pagamentos Pix e notas fiscais em XML.
Por que float quebra com dinheiro
float é binário de ponto flutuante (padrão IEEE 754). Números como 0,1 e 0,2 são dízimas em binário — assim como 1/3 é 0,333… em decimal — e ficam guardados como aproximações:
>>> 0.1 + 0.2
0.30000000000000004
>>> 0.1 + 0.2 == 0.3
False
>>> round(2.675, 2)
2.67 # não é 2.68!
Três centavos de erro numa linha parecem inofensivos. Multiplique por milhares de itens, juros compostos ou notas fiscais e o extrato não fecha. Por isso o padrão de mercado é: float para medição e ciência, Decimal para dinheiro.
Decimal: o básico que resolve
from decimal import Decimal
Decimal("0.1") + Decimal("0.2") # Decimal('0.3') — exato
Decimal("19.90") * 3 # Decimal('59.70')
Decimal("0.1") + Decimal("0.2") == Decimal("0.3") # True
A regra número 1: sempre crie Decimal a partir de string (ou int). Decimal(0.1) herda a imprecisão do float:
Decimal(0.1) # Decimal('0.1000000000000000055511151231257827021181583404541015625')
Decimal(str(0.1)) # Decimal('0.1') — último recurso quando o dado já vem como float
Arredondamento: quantize + ROUND_HALF_UP
from decimal import Decimal, ROUND_HALF_UP, ROUND_HALF_EVEN, ROUND_DOWN
CENTO = Decimal("0.01")
def para_moeda(valor: Decimal) -> Decimal:
return valor.quantize(CENTO, rounding=ROUND_HALF_UP)
para_moeda(Decimal("2.675")) # Decimal('2.68') ← o que o comércio espera
para_moeda(Decimal("2.674")) # Decimal('2.67')
# desconto sem arredondar para cima nunca (promoção "só 1 centavo a menos")
(Decimal("99.99") * Decimal("0.9")).quantize(CENTO, rounding=ROUND_DOWN) # 89.99
- ROUND_HALF_UP: 0,005 sobe — o arredondamento do dia a dia comercial e do Pix.
- ROUND_HALF_EVEN (banqueiro): 0,005 vai ao par mais próximo — padrão do
round()nativo e de contabilidade/fintechs; use quando a regra de negócio pedir. - ROUND_DOWN / ROUND_UP: corta sem “achismo”, ideal para descontos e taxas em favor de uma das partes.
Guarde o valor arredondado uma única vez (na gravação ou no cálculo final) e não re-arredonde resultados intermediários — cada quantize extra pode deslocar centavos.
Formatar reais (R$) corretamente
Para exibição no padrão brasileiro — ponto de milhar, vírgula decimal:
def brl(valor: Decimal) -> str:
texto = f"{valor:,.2f}" # 1,234,567.89
texto = texto.replace(",", "X").replace(".", ",").replace("X", ".")
return f"R$ {texto}"
brl(Decimal("1234567.89")) # 'R$ 1.234.567,89'
Para relatórios e dashboards em Streamlit, o pacote babel faz isso pronto:
from babel.numbers import format_currency
format_currency(Decimal("1234567.89"), "BRL", locale="pt_BR") # 'R$ 1.234.567,89'
Babel é dependência externa (pip install babel) — instale no ambiente virtual do projeto. Nunca guarde o texto formatado: persista o Decimal (ou centavos int) e formate só na borda da interface.
Exemplos práticos
Rateio de conta em N parcelas sem perder centavos
O clássico 100,00 ÷ 3 = 33,333… Como dividir sem que a soma dê 99,99 ou 100,02? Trabalhe em centavos inteiros e distribua o resto:
from decimal import Decimal
def ratear(total: Decimal, partes: int) -> list[Decimal]:
centavos = int(total.scaleb(2).to_integral_value()) # 100.00 -> 10000
base, resto = divmod(centavos, partes)
return [Decimal(base + (1 if i < resto else 0)).scaleb(-2) for i in range(partes)]
parcelas = ratear(Decimal("100.00"), 3) # [33.34, 33.33, 33.33]
sum(parcelas) # Decimal('100.00') — fecha!
Desconto com imposto sem erro acumulado
from decimal import Decimal, ROUND_HALF_UP
preco = Decimal("249.90")
desconto = (preco * Decimal("0.15")).quantize(Decimal("0.01"), rounding=ROUND_HALF_UP)
liquido = preco - desconto # 212.42
pis = (liquido * Decimal("0.0065")).quantize(Decimal("0.01"), rounding=ROUND_HALF_UP)
total_final = liquido - pis
Cada tributo calculado e arredondado com a regra explícita — o que as notas fiscais em XML/NFe exigem.
Ler preços de CSV/JSON sem corromper
import csv
from decimal import Decimal
with open("produtos.csv", newline="", encoding="utf-8") as f:
precos = [Decimal(linha["preco"]) for linha in csv.DictReader(f)]
Se a fonte for JSON, os números chegam como float do parser padrão — se a precisão importa, processe com parse_float=Decimal:
import json
from decimal import Decimal
pedido = json.loads(texto, parse_float=Decimal)
Detalhes de leitura de arquivos no guia de CSV com Python.
Decimal × float × int em centavos × py-moneyed
| Preciso de… | Use | Exemplo |
|---|---|---|
| Preço, saldo, cobrança, nota fiscal | Decimal (de string) | Decimal("19.90").quantize(Decimal("0.01")) |
| Medição científica, estatística, gráficos | float | 0.1 + 0.2 com tolerância; veja statistics |
| Sistema de pontuação/moeda única (centavos) | int de centavos | total_centavos // 3 |
| Multimoeda com conversão e objetos prontos | py-moneyed | Money("19.90", "BRL") |
intem centavos é a tática mais usada em sistemas de pagamento (Stripe, gateways Pix): impossível ter erro de ponto flutuante porque não existe ponto. Converta na entrada, some, converta na saída — oratearacima já faz isso por baixo.py-moneyedjuntaDecimal+ código ISO da moeda em um objetoMoney, com conversão via taxas. Vale para sistemas multimoeda; para um sistema só em reais,Decimalpuro é suficiente e sem dependências.floatcontinua correto para ciência de dados e análise com pandas — aí o erro de máquina é ordens de magnitude menor que a precisão dos dados.
Erros comuns
Decimal(0.1)a partir de float — herda 17 dígitos de imprecisão; useDecimal("0.1")ouDecimal(str(x)).round()para dinheiro — arredonda ao par (banqueiro) e sobre float dá 2.67 onde se espera 2.68; usequantize(..., ROUND_HALF_UP).- Somar
floate só converter no final — a imprecisão já foi acumulada; converta na entrada do cálculo. - Comparar
Decimalcomfloat—Decimal("0.1") == 0.1éFalse; converta antes de comparar. - Arredondar em cada passo — cada
quantizeintermediário desloca centavos; arredonde uma vez, na fronteira. - Guardar “R$ 1.234,56” no banco — persista valor numérico (
Decimal/centavos) e formate só na exibição.
Checklist rápido
- Todo valor monetário nasce de string (ou int de centavos), nunca de float?
- Arredondamento é
quantize(Decimal("0.01"), ROUND_HALF_UP)(ou regra explícita de negócio)? - Formatação em R$ acontece só na exibição (f-string ou babel)?
- Divisões/rateios somam centavos inteiros e distribuem o resto?
- JSON de origem financeira é lido com
parse_float=Decimal? - Testes cobrem os casos de borda (0,005; divisão por 3; desconto sobre valor já arredondado)?
Conclusão
Dinheiro em Python é Decimal: criado de string, arredondado uma única vez com quantize e ROUND_HALF_UP, dividido em centavos inteiros quando há rateio, e formatado em R$ apenas na apresentação. float fica para ciência; int de centavos brilha em sistemas de pagamento; py-moneyed resolve multimoeda. Fixar essas três regras elimina a classe inteira de bugs de centavo — exatamente o tipo de erro que conciliação financeira e cobrança via Pix não perdoam.
Próximos passos naturais neste site: conciliação financeira com Python, integração de pagamentos Pix, notas fiscais XML/NFe e estatística descritiva com statistics.