Para calcular dinheiro em Python, use Decimal criado a partir de texto, defina uma regra de arredondamento e normalize o resultado com quantize(). Essa é a resposta direta: Decimal("19.90") preserva o valor decimal informado, enquanto um float pode armazenar uma aproximação binária. Em cobranças, impostos, juros e conciliações, uma diferença aparentemente mínima pode se acumular ou impedir que totais fechem.
O módulo decimal pertence à biblioteca padrão, portanto não exige instalação. Ele implementa aritmética decimal com precisão configurável, sinais especiais e diferentes modos de arredondamento. Isso não torna qualquer cálculo automaticamente correto: moeda, escala, momento do arredondamento e regra de negócio ainda precisam ser explícitos. Se você está revisando fundamentos, consulte também variáveis e tipos de dados em Python.
Por que float não é a melhor representação de dinheiro?
Números float seguem o padrão de ponto flutuante binário. Frações simples em base dez, como 0,1, normalmente não têm representação binária finita. Por isso, o resultado abaixo não é exatamente 0,3:
print(0.1 + 0.2)
## 0.30000000000000004
Isso não é um defeito do Python. É uma propriedade da representação explicada no tutorial oficial sobre ponto flutuante. Para medições científicas, essa aproximação costuma ser adequada e eficiente. Para valores que devem obedecer a casas decimais e regras contábeis, Decimal comunica melhor o domínio.
from decimal import Decimal
subtotal = Decimal("0.10") + Decimal("0.20")
print(subtotal) # 0.30
print(subtotal == Decimal("0.30")) # True
A construção por string é essencial. Decimal(0.1) importa a aproximação que já existe no float e produz muitas casas. Quando a entrada chega como JSON, formulário ou CSV, mantenha o texto até convertê-lo. Para compreender validação e conversões de entrada, o guia de tratamento de erros em Python complementa esta prática.
Arredondamento monetário com quantize
quantize() ajusta o expoente do número ao modelo fornecido. Para uma moeda com duas casas, use Decimal("0.01"). O modo ROUND_HALF_UP arredonda empates para longe de zero e aparece em muitas regras comerciais; ROUND_HALF_EVEN, padrão do contexto decimal, reduz viés em séries longas. A escolha deve vir do contrato, da legislação ou do setor contábil, não de preferência pessoal.
from decimal import Decimal, ROUND_HALF_UP
CENTAVO = Decimal("0.01")
def moeda(valor: Decimal) -> Decimal:
return valor.quantize(CENTAVO, rounding=ROUND_HALF_UP)
preco = Decimal("37.95")
quantidade = 3
desconto = Decimal("0.10")
total = moeda(preco quantidade (Decimal("1") - desconto))
print(total) # 102.47
Arredonde no ponto definido pela regra. Arredondar cada item e depois somar pode gerar um total diferente de somar valores precisos e arredondar apenas a fatura. Ambos podem ser válidos, mas representam políticas distintas. Registre a decisão e cubra casos de empate com testes. O artigo sobre testes unitários com unittest ajuda a transformar exemplos contábeis em regressões automatizadas.
Precisão, contexto e operações seguras
O contexto controla precisão, arredondamento, limites e sinais. Precisão não significa número de casas monetárias: ela limita dígitos significativos usados nas operações. Uma divisão de juros pode precisar de várias casas intermediárias antes da conversão final para centavos. Use localcontext() quando uma rotina exigir configuração própria, evitando alterar globalmente outras partes da aplicação.
from decimal import Decimal, localcontext, ROUND_HALF_EVEN
capital = Decimal("12500.00")
taxa_anual = Decimal("0.0875")
with localcontext() as ctx:
ctx.prec = 28
ctx.rounding = ROUND_HALF_EVEN
juros_diarios = capital * taxa_anual / Decimal("365")
juros_lancado = juros_diarios.quantize(Decimal("0.01"))
print(juros_lancado) # 3.00
A documentação oficial de decimal detalha contextos, sinais e todos os modos. A motivação do desenho está na PEP 327. Em projetos maiores, encapsule constantes e política monetária em um módulo pequeno; não espalhe strings de escala e modos de arredondamento.
Caso real: itens, imposto e total da fatura
Uma fatura precisa receber preços externos, multiplicar quantidades e aplicar imposto. O exemplo valida texto, calcula cada linha sem misturar float e devolve valores com duas casas:
from decimal import Decimal, InvalidOperation, ROUND_HALF_UP
CENTAVO = Decimal("0.01")
def decimal_monetario(texto: str) -> Decimal:
try:
valor = Decimal(texto)
except InvalidOperation as erro:
raise ValueError("valor monetário inválido") from erro
if not valor.is_finite() or valor < 0:
raise ValueError("valor monetário deve ser finito e positivo")
return valor
def calcularfatura(itens, aliquota: str):
itens = list(itens)
for , quantidade in itens:
if type(quantidade) is not int or quantidade <= 0:
raise ValueError("quantidade deve ser um inteiro positivo")
base = sum(
(decimal_monetario(preco) quantidade for preco, quantidade in itens),
start=Decimal("0"),
)
imposto = (base decimal_monetario(aliquota)).quantize(
CENTAVO, rounding=ROUND_HALF_UP
)
base = base.quantize(CENTAVO, rounding=ROUND_HALF_UP)
total = (base + imposto).quantize(CENTAVO, rounding=ROUND_HALF_UP)
return base, imposto, total
print(calcular_fatura([("19.90", 2), ("5.35", 1)], "0.075"))
(Decimal('45.15'), Decimal('3.39'), Decimal('48.54'))
O código rejeita NaN e infinito, valores que Decimal aceita mas que raramente fazem sentido em um pagamento. Em um sistema internacional, guarde também o código ISO da moeda: somar 10 BRL a 10 USD é um erro de domínio, embora os números sejam compatíveis. Para persistência, defina uma coluna decimal com escala apropriada; veja o guia de SQLite com Python para decisões de armazenamento.
Erros comuns
- Criar a partir de float: prefira
Decimal("2.50")ou converta uma entrada textual validada. - Misturar Decimal e float: operações como soma são rejeitadas; mantenha uma representação única no fluxo monetário.
- Usar round sem política: explicite a escala e o modo com
quantize(). - Arredondar cedo demais: preserve precisão intermediária e arredonde no evento contábil correto.
- Ignorar moeda e unidade: um Decimal representa quantidade, não identifica BRL, USD ou centavos.
- Serializar como número JSON: alguns consumidores o converterão em float; contratos financeiros frequentemente usam string.
Checklist para produção
- Receba valores decimais como texto e valide formato, sinal e limites.
- Defina moeda, escala e modo de arredondamento por regra de negócio.
- Não misture
floatno mesmo caminho de cálculo. - Use precisão intermediária suficiente e arredonde no momento documentado.
- Teste empates, valores grandes, zero, negativos permitidos e rateios.
- Persista com tipo decimal adequado ou em unidades inteiras quando o domínio permitir.
- Registre os componentes do total para auditoria e conciliação.
Para melhorar a legibilidade dessas regras, aplique as práticas do guia de Clean Code em Python: nomes como aliquota, base e imposto são mais seguros do que cálculos anônimos em uma única expressão. A precisão técnica e a clareza do código precisam caminhar juntas.
Perguntas frequentes
Decimal é sempre melhor que float?
Não. float é adequado para computação científica, gráficos e medidas aproximadas. Decimal é indicado quando a representação decimal e o arredondamento definido pelo negócio são requisitos, como em dinheiro.
Posso usar centavos como int?
Sim, quando todas as operações usam uma escala fixa. Inteiros são simples e exatos, mas juros, câmbio, moedas com escalas diferentes e rateios exigem uma política adicional. Decimal costuma expressar esses casos com mais clareza.
Qual modo de arredondamento devo escolher?
Escolha o exigido pela regra contábil ou legal. ROUND_HALF_UP é comum no comércio, enquanto ROUND_HALF_EVEN reduz viés estatístico. Documente e teste a escolha.
Decimal garante que meu sistema financeiro está correto?
Não sozinho. Ele resolve a aritmética decimal, mas o sistema ainda precisa validar moeda, momento do arredondamento, impostos, persistência, concorrência e trilha de auditoria.