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.
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:
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:
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:
parcelas = Decimal(12)
centavos = Decimal(1990)
Evite esta forma:
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:
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"):
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:
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:
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 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:
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 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:
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.
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, 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:
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:
from decimal import getcontext
getcontext().prec = 6
Para uma operação isolada, use localcontext():
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:
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:
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 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:
import json
from decimal import Decimal
# json.dumps({"total": Decimal("19.90")}) # TypeError
Duas estratégias comuns são:
- enviar como string:
{"total": "19.90", "moeda": "BRL"}; - 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.
Como testar cálculos monetários
Testes devem verificar valores exatos e casos de fronteira:
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 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 comofloatacidental; - 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/DECIMALou 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 para observar onde essas habilidades aparecem no mercado brasileiro.
Equipe Python Brasil
Contribuidor do Python Brasil — Aprenda Python em Português