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.

10 min de leitura Equipe Python Brasil

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:

RegraComportamento resumidoUso possível
ROUND_HALF_UPempate vai para longe de zeroexpectativa comum em interfaces comerciais
ROUND_HALF_EVENempate vai para o vizinho parreduz viés em grandes conjuntos de cálculos
ROUND_DOWNdescarta o excesso em direção a zeroregras específicas que exigem truncamento
ROUND_UPafasta de zero se houver excessocená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:

  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.

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 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 para observar onde essas habilidades aparecem no mercado brasileiro.

E

Equipe Python Brasil

Contribuidor do Python Brasil — Aprenda Python em Português