How do you choose between decimal.Decimal, int minor units and float for money?
answer
- Rule one candidate out immediately
- Two serious candidates, different failure modes
- The guarantee lives at the edges
- Rounding is owned, not inherited
- The parts must sum to the whole
basics
~20 sRule float out: binary floating point cannot hold ordinary decimal amounts. Then choose between decimal.Decimal, which reads like the domain and carries scale, and integer minor units, which are exact by construction with no ambient context - and decide once where rounding happens.
solid answer
~50 sThe choice is really three decisions. **Representation**: `Decimal` models the domain directly, keeps scale and rounds under an explicit policy, while integer minor units are exact by construction, fast, trivially serializable and free of ambient context - at the cost of readability and of tracking each currency's exponent. `fractions.Fraction` is an excellent *intermediate* for exact ratios and a poor stored type. Float is excluded from the ledger path entirely. **Boundaries**: exactness only holds if values never become floats, so parse from strings, serialize as strings, and configure drivers to return exact types. **Policy**: rounding happens at named points, with a documented mode, and splits must reconcile - dividing an amount across a 4-person team needs a remainder rule so the parts sum to the whole. Wrap the choice in one money type so it stays changeable.
code
python · 12 linesimport json
from decimal import Decimal
amount = Decimal('19.99')
payload = json.dumps({'amount': str(amount)}) # {"amount": "19.99"}
restored = Decimal(json.loads(payload)['amount'])
print(payload, restored == amount)
try:
json.dumps({'amount': amount})
except TypeError as exc:
print('refused:', exc)go deeper
Take away the rule and the reason: money never lives in a float, and an amount is built from a string or an integer count of minor units. Know that 19.99 can be stored as Decimal('19.99') or as 1999.
Compare the two candidates concretely - scale and readability versus exactness with no ambient context - and explain why division is the operation that forces a decision in either representation.
Show the boundaries in a running system: parsing, driver configuration, serialization as strings, one rounding point. Be able to write the allocation rule that makes shares sum back to the total and test it as an invariant.
Own it as a system decision: one money type so the representation stays changeable, an enforceable rule keeping floats out of the ledger, the rounding and allocation policy written where finance can read it, and a clear answer for when the exact type stops at the reporting boundary.
### Start by eliminating Binary floating point is out of the ledger path. It cannot represent most decimal amounts, it has no concept of scale, and its rounding is not the rounding your finance rules specify. That is not a close call, and the interesting discussion begins after it. ### Decimal versus integer minor units **`decimal.Decimal`** reads like the domain: `Decimal('19.99')` is the price, it prints with its scale, and comparisons and sums behave the way a reviewer expects. It has a defined rounding vocabulary, most database adapters map it to and from an exact numeric column, and it is in the standard library. Its liabilities are the ambient context - precision and rounding read from thread-local state that some other module can change - the fact that division introduces rounding, and how easily a stray float slips in from a decoder or a driver. **Integer minor units** store 1999 and mean 19.99. Addition and multiplication by whole quantities are exact with no context anywhere; the values compare, hash, serialize and sum trivially; and they are fast. The price is that every division becomes an explicit `divmod` with a written-down rule for the remainder, that the currency exponent has to travel with the value because not every currency has two decimal places, and that raw ints in a codebase invite somebody to add a price to a quantity. A defensible default for most business applications is Decimal end to end. Integer minor units earn their place when the money path is hot, when values cross many process or language boundaries, or when you want zero ambient state in a system where several teams touch the code. **`fractions.Fraction`** belongs to neither role and to one job: exact ratios inside a calculation - a rate, a proportional split - lifted in at the start of a multi-step computation and converted back once at the end. Storing money as a Fraction throws away scale and invites unbounded denominators. ### The boundary is the real design work Whichever representation wins, the guarantee survives only as far as the boundaries hold. That means one parsing layer where external values become the exact type, and it means never constructing from a float. Amounts arriving as JSON should be strings on the wire; if they arrive as JSON numbers, decode them from their original text rather than after `float()` has already destroyed them. Outbound, serialize as strings - the standard JSON encoder refusing a Decimal with a TypeError is a feature, not an obstacle to route around with a float cast. Database drivers must be configured to return the exact type, and any float-only interface should be crossed deliberately, in one direction, for reporting rather than for settlement. ### Rounding is a policy, not a coding detail Decide where rounding happens and say so: typically once, at the point where an amount is settled or presented, with an explicit mode chosen by whoever owns the tax and contract rules rather than inherited from a default. Intermediate steps stay unrounded, which is where an exact rational intermediate is worth its cost. Rounding scattered through a calculation produces results nobody can reproduce, and no representation saves you from that. ### Allocation has to reconcile The evergreen example: a credit split across a 4-person team, or any amount divided into shares. Exactness of the type does not make the split come out even - a hundred units across three people has no exact answer at two decimal places in any representation. What you need is a documented allocation rule: compute the shares, compute the remainder, and distribute the leftover minor units by a stated policy such as largest remainder, so that the parts always sum back to the whole. Encode it once, and test it as an invariant - a property test asserting that the sum of the parts equals the total for random totals and party counts is worth more than any amount of hand-checking. ### Make the decision changeable and enforceable Put the representation behind a single small money type carrying the amount and the currency, with arithmetic on it, so that switching Decimal for minor units later is one module's problem rather than the whole codebase's. Give the ledger module a rule that no float may enter it, and make it a review or lint check rather than a convention. Run the money tests with the `Inexact` signal trapped, so any silent rounding fails a test instead of shipping. Document the currency exponent, the rounding mode and the allocation rule in one place that the finance side of the business can read. ### How to answer it Show that you know it is a systems decision, not a type preference: name the two serious candidates and why each wins, be explicit that the guarantee lives at the boundaries, and end on policy - where rounding happens, how remainders are allocated, and how the codebase is stopped from quietly reintroducing a float.
- What would make you pick integer minor units over Decimal?A hot money path where Decimal's cost shows, or a system where amounts cross many process and language boundaries and you want a representation with no ambient context and no serialization ambiguity. Integers also remove a whole class of bug - no stray float can be added to them silently. You accept explicit divmod for every division and carrying the currency exponent yourself.
- How do you stop a float from re-entering the money path six months from now?Make it structural rather than cultural: one money type that only accepts strings and ints, one parsing layer at the boundary, drivers configured to return exact types, and a lint or import rule for the ledger module. Then run the money tests with the Inexact signal trapped so any silent rounding fails the build rather than shipping as a one-cent discrepancy.
- Where does an exact rational intermediate actually pay for itself?In a multi-step calculation where each step would otherwise round: chained rates, proportional allocations, conversion factors. Carrying the intermediates as exact rationals means the single rounding happens where you chose it, at the settlement or presentation boundary. It stops paying when the chain is long enough that denominators grow, or when the calculation is in a hot loop.
Choosing the type is like choosing the units on a set of blueprints. Millimetres or metres both work, but the project fails on the drawings where somebody converted at the wrong moment and rounded twice.
saying these in an interview costs you the question
- Says float is acceptable if you round at the end
- Picks a type without naming where conversion happens
- Assumes an exact type makes splits reconcile automatically
- Serializes amounts as JSON numbers
- Leaves the rounding mode to whatever the default is
- Scatters rounding through intermediate calculation steps