---
title: "Decimal em Python: dinheiro, centavos e precisão financeira"
url: "https://python.dev.br/blog/python-decimal-moedas-precisao-financeira/"
markdown_url: "https://python.dev.br/blog/python-decimal-moedas-precisao-financeira.MD"
description: "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."
date: "2026-10-02"
author: "Equipe Python Dev BR"
---

# 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](/blog/conciliacao-financeira-python/), [pagamentos Pix](/blog/integracao-pagamentos-pix-python/) e [notas fiscais em XML](/blog/python-automacao-notas-fiscais-xml-nfe/).

## 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:

```python
>>> 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](/blog/conciliacao-financeira-python/). Por isso o padrão de mercado é: **float para medição e ciência, Decimal para dinheiro**.

## Decimal: o básico que resolve

```python
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:

```python
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

```python
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](/blog/integracao-pagamentos-pix-python/).
- **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:

```python
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](/guias/python-relatorios-dashboard-streamlit/), o pacote **babel** faz isso pronto:

```python
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](/guias/criando-virtual-environment/) 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:

```python
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

```python
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](/blog/python-automacao-notas-fiscais-xml-nfe/) exigem.

### Ler preços de CSV/JSON sem corromper

```python
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](/blog/trabalhando-com-json-python/), os números chegam como float do parser padrão — se a precisão importa, processe com `parse_float=Decimal`:

```python
import json
from decimal import Decimal

pedido = json.loads(texto, parse_float=Decimal)
```

Detalhes de leitura de arquivos no guia de [CSV com Python](/blog/python-csv-leitura-escrita-arquivos/).

## 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](/blog/python-statistics-media-mediana-desvio-padrao/) |
| 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")` |

- **`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](/guias/analise-dados-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](/blog/conciliacao-financeira-python/) e [cobrança via Pix](/blog/integracao-pagamentos-pix-python/) não perdoam.

Próximos passos naturais neste site: [conciliação financeira com Python](/blog/conciliacao-financeira-python/), [integração de pagamentos Pix](/blog/integracao-pagamentos-pix-python/), [notas fiscais XML/NFe](/blog/python-automacao-notas-fiscais-xml-nfe/) e [estatística descritiva com statistics](/blog/python-statistics-media-mediana-desvio-padrao/).
