For money calculations in Python, create Decimal values from strings, choose an explicit rounding rule, and normalize final amounts with quantize(). That is the practical answer: Decimal("19.90") preserves the decimal amount you entered, while a float usually stores a binary approximation. Tiny differences matter when invoices must reconcile, tax must follow a defined rule, or thousands of transactions are accumulated.

The decimal module is part of the standard library, so there is nothing to install. It provides configurable precision, rounding modes, signals, and exact decimal construction. It does not decide your business rules. You must still define the currency, number of minor units, rounding event, and treatment of negative values. Readers reviewing Python fundamentals can start with Python variables and data types.

Why float is risky for monetary amounts

A Python float uses binary floating-point representation. Many short base-ten fractions have no finite binary representation, so the nearest representable value is stored. The familiar example makes the issue visible:

print(0.1 + 0.2)
## 0.30000000000000004

This is not a Python bug. It is a consequence of the representation, clearly covered by the official floating-point tutorial. Floats remain excellent for scientific measurements and many performance-sensitive calculations. Money differs because users expect decimal digits and accounting systems require repeatable rounding.

from decimal import Decimal

subtotal = Decimal("0.10") + Decimal("0.20") print(subtotal) # 0.30 print(subtotal == Decimal("0.30")) # True

Construct from a string, not a float. Calling Decimal(0.1) faithfully imports the float's existing approximation and exposes a long decimal expansion. Keep API, CSV, and form inputs as text until validation and conversion. The guide to Python exception handling provides useful patterns for rejecting malformed input.

Round money explicitly with quantize

quantize() changes a value to the exponent of a template. For a currency with two minor digits, that template is Decimal("0.01"). ROUND_HALF_UP moves ties away from zero and is common in commercial rules. ROUND_HALF_EVEN, the default context behavior, reduces aggregate bias. Neither is universally correct; contracts, regulations, and accounting policy determine the answer.

from decimal import Decimal, ROUND_HALF_UP

CENT = Decimal("0.01")

def as_money(value: Decimal) -> Decimal: return value.quantize(CENT, rounding=ROUND_HALF_UP)

unit_price = Decimal("37.95") quantity = 3 discount = Decimal("0.10") total = as_money(unit_price quantity (Decimal("1") - discount)) print(total) # 102.47

Round at the event required by the domain. Rounding every line before summing can differ from summing precise lines and rounding the invoice once. Both policies exist in real systems. Document yours and test midpoint examples. The unittest guide shows how to turn accounting examples into regression tests.

Precision, context, and intermediate calculations

A decimal context controls significant-digit precision, rounding, exponent limits, and signals. Precision is not the same as currency scale. Interest division may need many intermediate digits even though the posted amount has two. Use localcontext() for a calculation-specific setting instead of changing global behavior for unrelated code.

from decimal import Decimal, localcontext, ROUND_HALF_EVEN

principal = Decimal("12500.00") annual_rate = Decimal("0.0875")

with localcontext() as ctx: ctx.prec = 28 ctx.rounding = ROUND_HALF_EVEN daily_interest = principal * annual_rate / Decimal("365")

posted_interest = daily_interest.quantize(Decimal("0.01")) print(posted_interest) # 3.00

The official decimal documentation explains contexts, traps, and every rounding option. The design rationale appears in PEP 327. In a larger service, keep scale constants and rounding policy in one small domain module rather than scattering them across handlers.

Real case: invoice lines, tax, and total

An invoice receives external price strings, multiplies quantities, and applies tax. This example validates input, never mixes numeric representations, and returns normalized amounts:

from decimal import Decimal, InvalidOperation, ROUND_HALF_UP

CENT = Decimal("0.01")

def money_input(text: str) -> Decimal: try: value = Decimal(text) except InvalidOperation as error: raise ValueError("invalid monetary value") from error if not value.is_finite() or value < 0: raise ValueError("money must be finite and non-negative") return value

def calculate_invoice(items, taxrate: str): items = list(items) for , quantity in items: if type(quantity) is not int or quantity <= 0: raise ValueError("quantity must be a positive integer") subtotal = sum( (money_input(price) quantity for price, quantity in items), start=Decimal("0"), ) tax = (subtotal money_input(tax_rate)).quantize( CENT, rounding=ROUND_HALF_UP ) subtotal = subtotal.quantize(CENT, rounding=ROUND_HALF_UP) total = (subtotal + tax).quantize(CENT, rounding=ROUND_HALF_UP) return subtotal, tax, total

print(calculate_invoice([("19.90", 2), ("5.35", 1)], "0.075"))

(Decimal('45.15'), Decimal('3.39'), Decimal('48.54'))

The validation rejects NaN and infinity. Decimal supports them, but payment amounts generally should not. International systems must also carry an ISO currency code; adding 10 USD to 10 BRL is a domain error despite compatible numeric types. When persisting amounts, choose a decimal column with adequate precision and scale. The Python and SQLite guide is a useful introduction to persistence decisions.

Common mistakes

  • Constructing from float: use Decimal("2.50") or validated source text.
  • Mixing Decimal and float: keep one representation throughout the monetary path.
  • Calling round without a policy: state scale and rounding mode with quantize().
  • Rounding too early: retain intermediate precision until the specified posting event.
  • Ignoring currency: Decimal stores a quantity, not whether it is USD, EUR, or cents.
  • Sending JSON numbers: a consumer may parse them as floats; financial APIs often send decimal strings.

Production checklist

  • Accept decimal amounts as strings and validate syntax, sign, and limits.
  • Define currency, scale, and rounding mode as domain policy.
  • Keep floats out of the complete money calculation path.
  • Use enough intermediate precision and round at documented events.
  • Test ties, large values, zero, allowed negatives, tax, and allocation remainders.
  • Store a suitable decimal type or integer minor units when the domain permits it.
  • Retain calculation components for audit and reconciliation.

Readable rules are easier to audit. The recommendations in Python Clean Code apply directly: names such as tax_rate, subtotal, and posted_interest are safer than an unexplained one-line formula. Arithmetic precision and maintainability are separate requirements, and production software needs both.

Frequently asked questions

Is Decimal always better than float?

No. Float is appropriate for scientific calculations, graphics, and approximate measurements. Decimal is preferable when exact decimal representation and business-defined rounding are requirements, especially for money.

Can I store money as integer cents?

Yes, for a fixed-scale domain. Integers are simple and exact, but exchange rates, interest, allocations, and currencies with different minor units need extra rules. Decimal often expresses those cases more directly.

Which rounding mode should I use?

Use the mode required by accounting or law. ROUND_HALF_UP is common commercially, while ROUND_HALF_EVEN reduces statistical bias. Document the decision and test ties.

Does Decimal make a financial system correct?

Not by itself. It addresses decimal arithmetic. Correct systems must also validate currencies, define rounding moments, persist safely, handle concurrency, and preserve an audit trail.