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.

02 Oct 2026 6 min de leitura Equipe Python Dev BR

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…UseExemplo
Preço, saldo, cobrança, nota fiscalDecimal (de string)Decimal("19.90").quantize(Decimal("0.01"))
Medição científica, estatística, gráficosfloat0.1 + 0.2 com tolerância; veja statistics
Sistema de pontuação/moeda única (centavos)int de centavostotal_centavos // 3
Multimoeda com conversão e objetos prontospy-moneyedMoney("19.90", "BRL")
  • int em 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 — o ratear acima já faz isso por baixo.
  • py-moneyed junta Decimal + código ISO da moeda em um objeto Money, com conversão via taxas. Vale para sistemas multimoeda; para um sistema só em reais, Decimal puro é suficiente e sem dependências.
  • float continua 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

  1. Decimal(0.1) a partir de float — herda 17 dígitos de imprecisão; use Decimal("0.1") ou Decimal(str(x)).
  2. round() para dinheiro — arredonda ao par (banqueiro) e sobre float dá 2.67 onde se espera 2.68; use quantize(..., ROUND_HALF_UP).
  3. Somar float e só converter no final — a imprecisão já foi acumulada; converta na entrada do cálculo.
  4. Comparar Decimal com float — Decimal("0.1") == 0.1 é False; converta antes de comparar.
  5. Arredondar em cada passo — cada quantize intermediário desloca centavos; arredonde uma vez, na fronteira.
  6. 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.

E
Equipe Python Dev BR

Contribuidor do Python Dev BR

Artigos relacionados