Handle money with Decimal¶
The question. You're working with money — prices, totals, tax, discounts — and you need every figure to be exact to the penny. A float can't do this (0.1 + 0.2 != 0.3), so the answer is Decimal from start to finish.
The pattern is always the same four steps: build from strings, do arithmetic in Decimal, quantize to two places at the end, and format for display. Below is the whole workflow.
Step 1: build amounts from strings¶
Always construct money Decimals from strings (or integers), never from floats — Decimal(0.1) imports the float's error, Decimal('0.10') is exact. Reading prices as text (from a form, a CSV, a database) keeps them exact end to end.
from decimal import Decimal
price = Decimal('19.99')
quantity = 3
print(price, quantity)
# from user/text input, this is already safe:
amounts = [Decimal(s) for s in ['12.50', '4.99', '0.75']]
print(amounts)
Step 2: do the arithmetic in Decimal¶
Decimal supports the usual operators and works with int, so multiplying by a quantity or summing a basket is exact. sum works too — give it Decimal('0') as the start value to keep the type.
from decimal import Decimal
price = Decimal('19.99')
subtotal = price * 3 # 59.97 — exact
basket = [Decimal('12.50'), Decimal('4.99'), Decimal('0.75')]
basket_total = sum(basket, Decimal('0'))
print(subtotal, basket_total) # 59.97 18.24
Step 3: apply tax/discounts, then quantize to pennies¶
Intermediate results can have many decimal places (20% of 59.97 is 11.994), and that's fine — keep full precision during the calculation. Only at the end do you round to two places, with ROUND_HALF_UP, using a pennies template.
from decimal import Decimal, ROUND_HALF_UP
PENNIES = Decimal('0.01')
subtotal = Decimal('19.99') * 3 # 59.97
tax = subtotal * Decimal('0.20') # 11.9940 — full precision kept
total = (subtotal + tax).quantize(PENNIES, rounding=ROUND_HALF_UP)
print(f'subtotal {subtotal}, tax {tax}, total {total}') # total 71.96
Rounding once at the end matters: if you round every intermediate value, the little roundings accumulate and your total can drift by a penny or two. Compute exact, round last.
Step 4: format for display¶
quantize guarantees two decimal places even when the amount is whole (5.00, not 5). For display with a currency symbol and thousands separators, format the Decimal directly — f-strings accept it.
from decimal import Decimal, ROUND_HALF_UP
amount = (Decimal('1234') + Decimal('56.7')).quantize(Decimal('0.01'), rounding=ROUND_HALF_UP)
print(amount) # 1290.70 — trailing zero kept
print(f'£{amount:,.2f}') # £1,290.70 — symbol + thousands separator
Splitting a bill without losing a penny¶
A classic exactness trap: £10.00 split three ways is £3.333... — you can't pay a third of a penny. Round each share down, then hand the leftover pennies to the first few payers, so the parts sum back to the whole.
from decimal import Decimal, ROUND_DOWN
def split_evenly(total, n):
total = Decimal(str(total)).quantize(Decimal('0.01'))
share = (total / n).quantize(Decimal('0.01'), rounding=ROUND_DOWN)
shares = [share] * n
remainder = total - share * n # the leftover pennies
pennies = int(remainder / Decimal('0.01'))
for i in range(pennies): # distribute them one each
shares[i] += Decimal('0.01')
return shares
shares = split_evenly('10.00', 3)
print(shares) # [Decimal('3.34'), Decimal('3.33'), Decimal('3.33')]
print(sum(shares)) # 10.00 — adds back exactly
The rules, in short¶
- Build money
Decimals from strings, never floats. - Keep the whole calculation in
Decimal— mixing in afloatraisesTypeError(a useful guardrail). - Keep full precision while computing;
quantizeto0.01once, at the end, withROUND_HALF_UP. - When splitting, round the parts and redistribute the remainder so they sum back to the total.
- Format with
f'£{amount:,.2f}'for display.