Why use decimal.Decimal instead of float for currency amounts in Python?
answer
- Money is base ten, floats are base two
- Some decimal amounts have no binary form
- Digits and an exponent, stored in ten
- Built from a string, exact and scaled
- Mixing with float raises TypeError
basics
~20 sA float stores a binary fraction, so an amount written as 0.10 is not held exactly and the tiny errors accumulate as you add. decimal.Decimal stores base-10 digits, so 0.10 built from the string '0.10' is exact and money totals stay exact.
solid answer
~50 sMoney is defined in base 10 with a fixed scale and legally specified rounding, and binary floating point can represent none of that exactly - only fractions with a power-of-two denominator survive, so 0.10 does not. `decimal.Decimal` implements base-10 arithmetic: a sign, a coefficient of decimal digits and an exponent. Built from a string or an int it is exact, and it keeps its scale, so `Decimal('19.90')` still prints two decimal places. Arithmetic runs under a context that defaults to 28 significant digits and round-half-even, and addition, subtraction and multiplication inside that budget are exact; only division and very long results round. Decimal also refuses to mix with float in arithmetic - `Decimal('1.1') + 0.1` raises TypeError - which forces the conversion to happen at a boundary you choose. The cost is speed and the discipline of keeping every amount a Decimal.
code
python · 5 linesfrom decimal import Decimal
total = sum(Decimal('0.10') for _ in range(10))
print(total, total == Decimal('1.00')) # 1.00 True
print(Decimal('19.99') * 3) # 59.97go deeper
Be ready to say in one breath why 0.10 has no exact binary form and what Decimal stores instead. Always construct from a string, and know that adding a float to a Decimal raises TypeError.
Explain the mechanics: sign, coefficient and decimal exponent; preserved trailing zeros; a context with 28 significant digits and round-half-even; exactness for plus, minus and times, and rounding at division. Name integer minor units as the alternative.
Show where the boundaries go in a real service: parse strings from the transport, never a float; keep the value Decimal end to end; round once at the output; serialize as a string. Be able to justify the performance cost of Decimal in the ledger path.
Own the policy across the codebase - one money type, one conversion boundary, one documented rounding point - and be able to defend Decimal against integer minor units in terms of readability, serialization, interop with float-only tooling and the cost of getting it wrong.
### The mismatch between money and binary floating point A Python `float` is an IEEE-754 binary64 value: a sign, a 53-bit significand and a power-of-two exponent. Every value it can hold is therefore some integer multiplied by a power of two. A decimal amount survives that encoding only when its fractional part has a denominator built from twos - a half, a quarter, an eighth. Amounts like 0.10, 0.01 or 19.99 have a factor of five in the denominator, so they land on the nearest binary neighbour instead of the value you wrote. Each individual gap is far below a cent, but money code adds thousands of amounts, multiplies by rates and then compares totals for equality, and that is exactly the workload that turns invisible gaps into a reconciliation report that is off by a cent. Money is also not just a number. It is a number *plus a scale* (two decimal places for most currencies, zero for some, three for others) *plus a rounding rule* that is often written into a contract or a tax code. A `float` carries none of that: it has no notion of 'two decimal places', and its own rounding is fixed at round-half-even on the binary value. ### What decimal.Decimal is `decimal.Decimal` implements the General Decimal Arithmetic specification. Internally a value is a sign, a coefficient of decimal digits and a decimal exponent, so `Decimal('19.90')` is the digits 1990 with exponent -2. Three consequences matter in an interview. First, **construction from a string or an int is exact**. `Decimal('0.10')` is the value you wrote, not an approximation, and the constructor does not round to the context's precision. Second, **scale is preserved**. Trailing zeros are significant: `Decimal('19.90')` prints as `19.90`, and `Decimal('0.10') * 3` is `0.30`, keeping two decimal places without any formatting step. That is why summing ten `Decimal('0.10')` values gives exactly `Decimal('1.00')`, comparing equal to `Decimal('1.00')`. Third, **arithmetic runs under a context**. The default context allows 28 significant digits and rounds half-to-even. Addition, subtraction and multiplication of realistic money values are well inside that budget and are therefore exact; division is where rounding actually appears, because `Decimal(1) / Decimal(3)` cannot be finite in base 10 either. Decimal is *exact for decimal amounts*, not magically exact for every ratio. ### The float firewall Decimal deliberately refuses to do arithmetic with float: `Decimal('1.1') + 0.1` raises `TypeError`. Comparisons across the two types are allowed and are answered correctly - `Decimal('0.5') == 0.5` is True because a half is exact in binary, while `Decimal('0.1') == 0.1` is False - but you cannot silently contaminate a Decimal computation by letting one float in. Treat that TypeError as a feature: it tells you where a boundary is missing, and the fix is to parse the incoming value as a string rather than casting a float you already lost precision on. ### Signals and traps Decimal reports exceptional conditions as signals. `InvalidOperation`, `DivisionByZero` and `Overflow` are trapped by default and raise; `Inexact` and `Rounded` are merely recorded as flags on the context so ordinary rounding does not interrupt your program. A money test suite can turn the `Inexact` trap on to make any silent rounding an outright failure. ### What it costs, and the alternative Decimal is implemented in C and is fast enough for business workloads, but it is still several times slower than float and uses more memory, so it is the wrong default for scientific or array-style numeric work where float is exactly right. It also requires end-to-end discipline: an amount that touches a float-only interface and comes back has already lost the guarantee. The other respectable choice for money is integer minor units - store 1999 cents in an `int` and do every division explicitly - which is exact by construction and has no ambient context, at the cost of readability and of tracking each currency's exponent yourself. ### Working rules Build every amount from a string or an int, never from a float literal. Keep the value a Decimal through the whole calculation. Choose the display scale and the rounding mode once, at the output boundary. And serialize amounts as strings, because `json.dumps` refuses a Decimal outright rather than quietly turning it back into a float.
- Does using decimal.Decimal make every result exact?No. It makes decimal *amounts* exact and keeps addition, subtraction and multiplication exact while the result fits the context precision. A ratio such as one third still has no finite base-10 form, so `Decimal(1) / Decimal(3)` rounds to the context's 28 significant digits. Exactness applies to representation and to the operations that stay inside the digit budget, not to division in general.
- What is the alternative to Decimal if you want money handled by integers?Store minor units: 1999 as an int meaning 19.99. Addition and multiplication by whole quantities are exact with no context involved, comparison and serialization are trivial, and it is fast. The cost is that every division becomes an explicit `divmod` with a documented rule for the remainder, and you must carry the currency's exponent yourself, since not every currency has two decimal places.
- Why does json.dumps refuse a Decimal, and what should you send instead?JSON has one number type and the standard encoder will not silently downgrade a Decimal to a float, so it raises TypeError. Send the amount as a string - `json.dumps({'amount': str(amount)})` - and rebuild it with `Decimal(payload['amount'])` on the other side. Encoding it as a JSON number hands the receiver a float and throws away the guarantee you paid for.
A float is a ruler marked only in halves, quarters and eighths: you can get very close to a centimetre mark but never land on it exactly. Decimal is a ruler marked in tenths, so the marks you actually use line up.
saying these in an interview costs you the question
- Claims float is fine for money if you round at the end
- Thinks Decimal makes every division exact
- Writes Decimal(19.99) from a float literal
- Mixes floats and Decimals and expects arithmetic to work
- Serializes amounts as JSON numbers rather than strings
- Uses Decimal everywhere, including array-heavy numeric code