---
title: "Decimal em Python: dinheiro, centavos e cálculos precisos"
url: "https://python.dev.br/blog/python-decimal-dinheiro-calculos-precisos/"
markdown_url: "https://python.dev.br/blog/python-decimal-dinheiro-calculos-precisos.MD"
description: "Aprenda Decimal em Python para calcular dinheiro, centavos, descontos, impostos e arredondamentos sem os erros de precisão comuns do tipo float."
date: "2026-07-27"
author: "Equipe Python Brasil"
---

# Decimal em Python: dinheiro, centavos e cálculos precisos

Aprenda Decimal em Python para calcular dinheiro, centavos, descontos, impostos e arredondamentos sem os erros de precisão comuns do tipo float.


O módulo `decimal` é a escolha mais segura da biblioteca padrão para representar **dinheiro, centavos, taxas e quantidades decimais em Python**. Ele evita os resíduos de precisão comuns do tipo `float`, permite escolher a regra de arredondamento e ajuda a tornar cálculos de preços, descontos, comissões e totais reproduzíveis.

A recomendação direta é: crie valores com `Decimal("19.90")`, nunca com `Decimal(19.90)`; arredonde no ponto definido pela regra do negócio usando `quantize(Decimal("0.01"))`; e não misture `Decimal` com `float`. Para sistemas simples que trabalham exclusivamente com centavos, `int` também é uma boa opção.

Neste guia, você vai montar o cálculo de um pedido brasileiro, tratar entradas como `R$ 1.234,56`, distribuir desconto sem perder centavos, integrar o tipo ao banco de dados e testar os casos que normalmente geram bugs.

## Por que 0,1 + 0,2 não resulta exatamente em 0,3?

O `float` do Python segue o padrão IEEE 754 de ponto flutuante binário. Ele representa números como combinações de potências de dois. Assim como `1 / 3` não termina em decimal, vários números decimais cotidianos não terminam em binário.

```python
print(0.1 + 0.2)
# 0.30000000000000004

print((0.1 + 0.2) == 0.3)
# False
```

Isso não é um defeito exclusivo do Python. É uma consequência da representação adotada por praticamente todas as linguagens e processadores modernos.

Para coordenadas, medições científicas, gráficos e aprendizado de máquina, a aproximação de `float` costuma ser adequada e muito eficiente. O problema aparece quando a unidade mínima importa. Em um pedido, uma diferença de R$ 0,01 pode impedir a conciliação com o meio de pagamento, alterar um relatório ou fazer a soma das parcelas divergir do total.

Com `Decimal`, os valores decimais do exemplo são representados exatamente:

```python
from decimal import Decimal

resultado = Decimal("0.1") + Decimal("0.2")

print(resultado)                 # 0.3
print(resultado == Decimal("0.3"))  # True
```

A regra não é “`float` é ruim”. A regra é escolher a representação de acordo com o domínio. Para dinheiro e aritmética decimal controlada, `Decimal` comunica melhor a intenção.

## Como criar Decimal sem carregar o erro do float

A maneira recomendada é fornecer uma string:

```python
from decimal import Decimal

preco = Decimal("49.90")
quantidade = Decimal("3")
total = preco * quantidade

print(total)  # 149.70
```

Também é seguro construir a partir de um inteiro:

```python
parcelas = Decimal(12)
centavos = Decimal(1990)
```

Evite esta forma:

```python
from decimal import Decimal

valor_incorreto = Decimal(0.1)
print(valor_incorreto)
# 0.1000000000000000055511151231257827021181583404541015625
```

O problema surgiu antes de `Decimal`: o Python primeiro criou o `float` aproximado `0.1` e depois converteu sua representação exata. `Decimal` não tem como adivinhar que você pretendia o texto `"0.1"`.

Se uma biblioteca externa devolve `float`, procure uma opção para receber texto, inteiro ou `Decimal`. Quando isso não for possível, `Decimal(str(valor_float))` costuma refletir a forma exibida ao usuário, mas a decisão deve ser consciente e testada na fronteira do sistema.

### Não misture Decimal e float

O Python impede algumas misturas para evitar resultados ambíguos:

```python
from decimal import Decimal

preco = Decimal("10.00")
# preco * 0.9  # TypeError

total = preco * Decimal("0.90")
print(total)  # 9.0000
```

Defina constantes decimais como strings e mantenha o fluxo inteiro no mesmo tipo. Essa regra também facilita a revisão do código.

## Como arredondar dinheiro com quantize

`Decimal` preserva casas intermediárias. O método `quantize()` ajusta o valor a um expoente desejado. Para centavos, use `Decimal("0.01")`:

```python
from decimal import Decimal, ROUND_HALF_UP

CENTAVOS = Decimal("0.01")
valor = Decimal("10") / Decimal("3")
final = valor.quantize(CENTAVOS, rounding=ROUND_HALF_UP)

print(valor)  # 3.333333333333333333333333333
print(final)  # 3.33
```

Declare a regra de arredondamento porque “arredondar” pode significar coisas diferentes:

| Regra | Comportamento resumido | Uso possível |
|---|---|---|
| `ROUND_HALF_UP` | empate vai para longe de zero | expectativa comum em interfaces comerciais |
| `ROUND_HALF_EVEN` | empate vai para o vizinho par | reduz viés em grandes conjuntos de cálculos |
| `ROUND_DOWN` | descarta o excesso em direção a zero | regras específicas que exigem truncamento |
| `ROUND_UP` | afasta de zero se houver excesso | cenários específicos definidos pelo domínio |

Não existe uma regra universal para toda operação financeira, fiscal ou contratual. A aplicação deve seguir a especificação do sistema integrado e a regra validada pelo responsável pelo negócio. O papel do `decimal` é implementar essa decisão de forma explícita.

### Quando fazer o arredondamento?

Arredondar cada item e arredondar apenas o total podem produzir resultados diferentes:

```python
from decimal import Decimal, ROUND_HALF_UP

CENTAVOS = Decimal("0.01")
precos = [Decimal("0.335"), Decimal("0.335"), Decimal("0.335")]

por_item = sum(
    (preco.quantize(CENTAVOS, rounding=ROUND_HALF_UP) for preco in precos),
    start=Decimal("0"),
)

no_total = sum(precos, start=Decimal("0")).quantize(
    CENTAVOS,
    rounding=ROUND_HALF_UP,
)

print(por_item)  # 1.02
print(no_total)  # 1.01
```

Por isso, o ponto do arredondamento precisa fazer parte da regra, da documentação e dos testes. Não espalhe chamadas a `round()` e `quantize()` aleatoriamente pelo projeto.

## Exemplo completo: subtotal, desconto e total de um pedido

Vamos modelar itens com `dataclass` e concentrar o arredondamento monetário em uma função:

```python
from dataclasses import dataclass
from decimal import Decimal, ROUND_HALF_UP

CENTAVOS = Decimal("0.01")
CEM = Decimal("100")


def dinheiro(valor: Decimal) -> Decimal:
    return valor.quantize(CENTAVOS, rounding=ROUND_HALF_UP)


@dataclass(frozen=True)
class ItemPedido:
    descricao: str
    preco_unitario: Decimal
    quantidade: Decimal

    @property
    def subtotal(self) -> Decimal:
        return dinheiro(self.preco_unitario * self.quantidade)


def calcular_pedido(
    itens: list[ItemPedido],
    desconto_percentual: Decimal = Decimal("0"),
) -> dict[str, Decimal]:
    subtotal = sum(
        (item.subtotal for item in itens),
        start=Decimal("0"),
    )
    desconto = dinheiro(subtotal * desconto_percentual / CEM)
    total = dinheiro(subtotal - desconto)

    return {
        "subtotal": dinheiro(subtotal),
        "desconto": desconto,
        "total": total,
    }


itens = [
    ItemPedido("Teclado", Decimal("199.90"), Decimal("1")),
    ItemPedido("Cabo USB", Decimal("24.95"), Decimal("2")),
]

resumo = calcular_pedido(itens, desconto_percentual=Decimal("10"))
print(resumo)
# {'subtotal': Decimal('249.80'),
#  'desconto': Decimal('24.98'),
#  'total': Decimal('224.82')}
```

A `dataclass` deixa a estrutura dos dados clara e imutável. O tutorial de [dataclasses em Python](/blog/dataclasses-python-guia-completo/) aprofunda validação, valores padrão e comparação entre objetos.

Em uma aplicação real, valide também que preço e quantidade não são negativos, que o desconto está no intervalo permitido e que a moeda do pedido é única. `Decimal` resolve a aritmética; ele não substitui regras de domínio.

## Como converter R$ 1.234,56 para Decimal

Uma interface brasileira normalmente recebe vírgula decimal e pode incluir ponto de milhar. `Decimal` espera ponto como separador decimal, então a entrada precisa ser normalizada.

Para um campo controlado que aceita apenas o formato brasileiro, uma função simples pode ser:

```python
from decimal import Decimal, InvalidOperation


def decimal_br(texto: str) -> Decimal:
    normalizado = (
        texto.strip()
        .removeprefix("R$")
        .strip()
        .replace(".", "")
        .replace(",", ".")
    )

    try:
        return Decimal(normalizado)
    except InvalidOperation as erro:
        raise ValueError(f"Valor monetário inválido: {texto!r}") from erro


print(decimal_br("R$ 1.234,56"))  # 1234.56
print(decimal_br("19,90"))        # 19.90
```

Essa função é deliberadamente restrita. Ela não deve tentar adivinhar simultaneamente formatos como `1,234.56`, `1 234,56` e `1.234`. Ambiguidade silenciosa é perigosa. Em APIs, prefira um contrato não localizado, como `"1234.56"`, e aplique a formatação brasileira apenas na apresentação.

Nunca remova caracteres arbitrários até “sobrar um número”. Uma entrada inválida deve falhar com mensagem clara. Para APIs maiores, modelos com [Pydantic](/blog/pydantic-validacao-dados-python/) ajudam a centralizar essa validação.

## Como formatar Decimal em reais

A formatação da interface deve ser separada do valor usado nos cálculos:

```python
from decimal import Decimal


def formatar_reais(valor: Decimal) -> str:
    formatado = f"{valor:,.2f}"
    brasileiro = (
        formatado
        .replace(",", "TEMP")
        .replace(".", ",")
        .replace("TEMP", ".")
    )
    return f"R$ {brasileiro}"


print(formatar_reais(Decimal("1234.5")))   # R$ 1.234,50
print(formatar_reais(Decimal("24999.90"))) # R$ 24.999,90
```

Para aplicações internacionalizadas, use uma biblioteca de localização e informe explicitamente idioma e moeda. Não armazene `"R$ 1.234,50"` como se fosse o valor numérico; armazene o número e, quando necessário, o código da moeda, como `BRL`.

## Como dividir um desconto sem perder um centavo

Dividir R$ 10,00 entre três itens produz R$ 3,333... Se cada parcela for arredondada para R$ 3,33, a soma será R$ 9,99. Uma estratégia determinística é calcular as parcelas, medir a diferença e distribuir os centavos restantes.

```python
from decimal import Decimal, ROUND_DOWN

CENTAVOS = Decimal("0.01")


def dividir_valor(total: Decimal, partes: int) -> list[Decimal]:
    if partes <= 0:
        raise ValueError("partes deve ser maior que zero")

    parcela_base = (total / partes).quantize(
        CENTAVOS,
        rounding=ROUND_DOWN,
    )
    parcelas = [parcela_base for _ in range(partes)]

    diferenca = total - sum(parcelas, start=Decimal("0"))
    centavos_restantes = int(diferenca / CENTAVOS)

    for indice in range(centavos_restantes):
        parcelas[indice] += CENTAVOS

    return parcelas


parcelas = dividir_valor(Decimal("10.00"), 3)
print(parcelas)  # [Decimal('3.34'), Decimal('3.33'), Decimal('3.33')]
print(sum(parcelas, start=Decimal("0")))  # 10.00
```

Em pedidos, você pode distribuir a diferença pelo maior item, pela ordem original ou por uma regra proporcional. Escolha um critério estável para que recalcular a mesma operação gere o mesmo resultado.

Esse detalhe é importante em rotinas de [conciliação financeira com Python](/blog/conciliacao-financeira-python/), nas quais o total interno precisa bater com parcelas, recebíveis ou registros importados.

## Contexto, precisão e armadilhas globais

O módulo mantém um contexto com precisão e regra padrão:

```python
from decimal import getcontext

contexto = getcontext()
print(contexto.prec)      # normalmente 28
print(contexto.rounding)  # normalmente ROUND_HALF_EVEN
```

Alterar o contexto global afeta operações executadas depois no mesmo contexto. Isso pode surpreender outras partes da aplicação:

```python
from decimal import getcontext

getcontext().prec = 6
```

Para uma operação isolada, use `localcontext()`:

```python
from decimal import Decimal, localcontext

with localcontext() as contexto:
    contexto.prec = 50
    resultado = Decimal("1") / Decimal("7")
    print(resultado)
```

A precisão define a quantidade de algarismos significativos usada nas operações; ela não significa “duas casas decimais”. Para garantir centavos, continue usando `quantize()`.

Outra armadilha é comparar representações textuais em vez de valores. `Decimal("2.0")` e `Decimal("2.00")` têm o mesmo valor numérico, embora preservem expoentes diferentes:

```python
from decimal import Decimal

print(Decimal("2.0") == Decimal("2.00"))  # True
print(str(Decimal("2.0")))                # 2.0
print(str(Decimal("2.00")))               # 2.00
```

Se a quantidade de casas faz parte da exibição, formate-a na saída. Se faz parte de uma regra, normalize com `quantize()`.

## Decimal ou inteiro em centavos?

Representar R$ 19,90 como `1990` centavos também evita ponto flutuante:

```python
preco_centavos = 1990
quantidade = 3
total_centavos = preco_centavos * quantidade

print(total_centavos)  # 5970
```

Use inteiros quando:

- todos os valores têm uma unidade mínima fixa;
- o sistema externo trabalha em centavos;
- não há taxas intermediárias que exigem várias casas;
- a equipe prefere um modelo explícito de menor unidade.

Use `Decimal` quando:

- há preços por peso, consumo ou unidade fracionária;
- taxas e cálculos intermediários precisam de mais casas;
- os dados chegam como números decimais de banco ou API;
- a regra de arredondamento precisa ser aplicada em etapas específicas.

Não escolha com base apenas em performance. Para a maioria dos sistemas administrativos, clareza e consistência importam mais. O maior risco é misturar `float`, inteiro em centavos e `Decimal` sem fronteiras definidas.

## Banco de dados, JSON e APIs

Bancos relacionais normalmente oferecem tipos exatos como `NUMERIC` ou `DECIMAL`. Defina precisão e escala adequadas ao domínio, por exemplo `NUMERIC(12, 2)` para um intervalo monetário específico. Evite colunas `FLOAT` para dinheiro.

Ao ler com um driver ou ORM, confirme se a coluna chega como `Decimal`. O guia de [SQLAlchemy 2](/blog/sqlalchemy-2-orm-moderno-python/) mostra como modelar dados e controlar tipos no acesso ao banco.

JSON não possui um tipo decimal padronizado. Serializar `Decimal` diretamente com o módulo `json` gera erro:

```python
import json
from decimal import Decimal

# json.dumps({"total": Decimal("19.90")})  # TypeError
```

Duas estratégias comuns são:

1. enviar como string: `{"total": "19.90", "moeda": "BRL"}`;
2. enviar a menor unidade: `{"total_centavos": 1990, "moeda": "BRL"}`.

Converter para `float` apenas para satisfazer o serializador reintroduz a aproximação. Documente o contrato da API e mantenha a conversão nas bordas. Veja também o tutorial de [JSON em Python](/blog/trabalhando-com-json-python/).

## Como testar cálculos monetários

Testes devem verificar valores exatos e casos de fronteira:

```python
from decimal import Decimal


def test_calcular_pedido_com_desconto():
    itens = [
        ItemPedido("Produto A", Decimal("10.00"), Decimal("2")),
        ItemPedido("Produto B", Decimal("5.50"), Decimal("1")),
    ]

    resultado = calcular_pedido(
        itens,
        desconto_percentual=Decimal("10"),
    )

    assert resultado == {
        "subtotal": Decimal("25.50"),
        "desconto": Decimal("2.55"),
        "total": Decimal("22.95"),
    }


def test_divisao_preserva_total():
    parcelas = dividir_valor(Decimal("10.00"), 3)

    assert parcelas == [
        Decimal("3.34"),
        Decimal("3.33"),
        Decimal("3.33"),
    ]
    assert sum(parcelas, start=Decimal("0")) == Decimal("10.00")
```

Inclua nos testes:

- valor zero;
- desconto zero e desconto máximo permitido;
- empate de arredondamento, como `1.005`;
- número negativo, caso estorno seja permitido;
- entradas inválidas e separadores ambíguos;
- grande quantidade de itens;
- soma das parcelas igual ao total original.

O artigo sobre [testes unitários em Python](/blog/testes-unitarios-python/) explica fixtures, parametrização e organização da suíte.

## Checklist para usar Decimal em produção

Antes de publicar uma rotina de preços ou totais, confirme:

- [ ] todos os valores entram como string, inteiro ou `Decimal`, nunca como `float` acidental;
- [ ] a unidade e a moeda estão explícitas;
- [ ] a regra e o momento do arredondamento foram definidos;
- [ ] `quantize()` é aplicado nas fronteiras previstas;
- [ ] a entrada brasileira é validada, não apenas “limpa”;
- [ ] o banco usa `NUMERIC`/`DECIMAL` ou centavos inteiros;
- [ ] o contrato JSON usa string decimal ou menor unidade;
- [ ] parcelas e rateios preservam o total;
- [ ] casos de meio centavo e valores negativos têm testes;
- [ ] logs não expõem dados pessoais ou detalhes desnecessários da operação.

## Conclusão

`Decimal` resolve um problema simples de explicar e caro de descobrir tarde: **valores decimais importantes não devem depender da aproximação binária de `float`**. Crie números a partir de strings, mantenha um único tipo durante o cálculo, declare o arredondamento com `quantize()` e trate formatação, banco e JSON como fronteiras explícitas.

Para um projeto de portfólio, implemente um orçamento, carrinho ou conciliador que importe valores, aplique descontos, distribua centavos e produza um relatório verificável. Esse exercício demonstra modelagem, testes e atenção a regras reais — competências úteis em vagas de backend, automação e dados. Consulte as [vagas de Python](/vagas/) para observar onde essas habilidades aparecem no mercado brasileiro.
