skip to content

Numbers and Precision

Money, floats and randomness: Decimal and Fraction for exact arithmetic, math and statistics helpers, and seeded generators you can reproduce. Interviewers open with why 0.1 + 0.2 is not 0.3.

part ofPythonoverview, primer and where to startread it →
on this pageshow

questions

12

Why should a decimal.Decimal be built from a string rather than from a float?

level: juniorimportance: must knowfreq 62%

answer

  1. The error is already in the float
  2. The constructor copies, it does not clean
  3. Binary base two cannot hold one tenth
  4. Decimal(0.1) against Decimal('0.1')
  5. FloatOperation trap makes it raise

basics

~10 s

Decimal(0.1) converts the float exactly, so it stores 0.1000000000000000055511151231257827..., the binary approximation the float already was. Decimal("0.1") parses the decimal digits you actually wrote and stores exactly one tenth.

solid answer

~40 s

A binary float cannot represent one tenth; `0.1` is really 0.1000000000000000055511151231257827... The `decimal.Decimal` constructor is an *exact* conversion, so `Decimal(0.1)` faithfully copies that error into the Decimal instead of removing it, while `Decimal("0.1")` parses the decimal literal and stores exactly one tenth. So the rule is that a Decimal must be built from a string, an int, or another Decimal — never from a float — and the discipline has to reach back to where the value enters the process: read amounts out of JSON, a form field or a database driver as text or as integer minor units, not as floats. If you want the mistake to be loud rather than silent, enable the `FloatOperation` trap in the decimal context and constructing a Decimal from a float raises.

code

pycon · 9 lines
pycon
>>> from decimal import Decimal
>>> Decimal(0.1)
Decimal('0.1000000000000000055511151231257827021181583404541015625')
>>> Decimal("0.1")
Decimal('0.1')
>>> sum([Decimal("0.1")] * 3) == Decimal("0.3")
True
>>> sum([0.1] * 3) == 0.3
False

go deeper

for a junior

Be ready to state the rule and the reason in one breath: build a Decimal from a string or an int, never from a float, because the float is already an approximation and the constructor copies it exactly rather than fixing it.

for a middle

Explain the mechanics: binary64 stores integers over powers of two, so one tenth is not representable, and Decimal stores a coefficient with a base-ten exponent. Show the fifty-five-digit output and know that the constructor ignores context precision.

for a senior

Show where you enforce this in a real codebase — parsing amounts as text at the process boundary, the FloatOperation trap turned on in tests, and a clear answer for a float that arrives from a system you do not control.

for a principal

Own the house rule for how money is represented across services: Decimal end to end against integer minor units, where conversion happens, and how the choice survives the JSON, database and message-queue boundaries in between.

### The float you started with is already wrong A Python `float` is an IEEE-754 binary64 value: a sign, an 11-bit exponent and a 53-bit significand, all in base two. Every value it can hold is some integer divided by a power of two. One tenth is not such a value — 1/10 in binary is an infinitely repeating expansion, exactly the way 1/3 repeats in decimal — so the literal `0.1` in your source is silently replaced by the nearest binary64 value, which written out in full is `0.1000000000000000055511151231257827021181583404541015625`. Nothing later in the program can recover the tenth you meant, because the tenth was never stored. ### The Decimal constructor is exact, and that is the trap `decimal.Decimal` is a base-ten type: it stores a sign, an integer coefficient and a decimal exponent, so one tenth is representable exactly as coefficient 1 and exponent -1. The catch is that the constructor never rounds and never guesses. Given a `str`, it parses the decimal digits you wrote. Given a `float`, it performs an exact base conversion of the binary value — and since that binary value is a dyadic rational, its exact decimal form is finite, which is why you get fifty-five digits of noise rather than a friendly `0.1`: ```pycon >>> from decimal import Decimal >>> Decimal(0.1) Decimal('0.1000000000000000055511151231257827021181583404541015625') >>> Decimal("0.1") Decimal('0.1') ``` People often read `Decimal(0.1)` as "convert this to a decimal type and clean it up". It does the opposite: it preserves, permanently and visibly, the error that the float already carried. Passing through a float on the way in defeats the entire point of using Decimal. ### Why it matters in practice Base ten is not merely prettier; it makes the sums people expect actually hold. Three tenths added as floats is not three tenths, while three tenths added as Decimals is: ```pycon >>> sum([0.1] * 3) == 0.3 False >>> sum([Decimal("0.1")] * 3) == Decimal("0.3") True ``` That is the property money, tax rates, invoice lines and any regulated arithmetic depend on. The usual production discipline is that a monetary value is never a float anywhere in the process: it arrives as a string (JSON text, an HTML form field, a CSV cell) or as an integer number of minor units, and is turned into a Decimal at the boundary. Watch the boundary carefully — a JSON parser will hand you a float for `1.15` unless you tell it not to, and a database driver may or may not hand back Decimals for a numeric column. ### Making the mistake loud The decimal module has a signal for exactly this. Turning on the `FloatOperation` trap in the current context makes building a Decimal from a float raise instead of quietly succeeding, which turns a class of silent data bugs into a stack trace during tests: ```python from decimal import Decimal, FloatOperation, localcontext with localcontext() as ctx: ctx.traps[FloatOperation] = True try: Decimal(0.1) except FloatOperation: print("refused to build a Decimal from a float") ``` ### What to do when a float is genuinely all you have Sometimes the float is upstream and unfixable. Then you must decide, explicitly, what the float was supposed to mean. If it came from human decimal data, round it back to the number of places that data has — `Decimal(str(x))` uses `repr`'s shortest round-tripping form, and `Decimal(x).quantize(Decimal("0.01"))` rounds under the context's rounding rule. If it came from a physical measurement, the extra digits are meaningless anyway. What you must not do is copy the float in exactly and then pretend the result is exact. ### Where floats sneak in The rule is easy to state and easy to violate, because the float usually appears before your code runs. `json.loads` produces a float for every JSON number with a point in it, so an amount is already damaged by the time you construct a Decimal from it; the fix is the parser hook, `json.loads(payload, parse_float=Decimal)`, which builds Decimals straight from the text the wire carried. A CSV reader hands you strings, which is safer, provided nobody helpfully calls `float()` on the column first. A database driver may return Decimals for a numeric column or floats for a double-precision one, so the column type upstream decides what you receive. Review these three boundaries and the problem largely disappears; review only the arithmetic and it will not. ### Two related details worth knowing First, the constructor is also exempt from the context's precision: `Decimal("1.23456789")` keeps all nine digits no matter what `prec` says, because only *operations* apply the context. If you want the context applied at construction time, use the context's own `create_decimal` method. Second, the constructor accepts `int`, `str`, `tuple` and `Decimal` — an int is always safe, which is why storing money as integer cents and dividing once at the edge is a legitimate alternative design to using Decimal end to end.

  • A third-party client hands you a float and you cannot change it. How do you turn it into a Decimal responsibly?
    Decide what the float was meant to represent and say so in code. If it came from human decimal data, go through text — Decimal(str(x)) uses repr's shortest round-tripping digits — or convert exactly and then quantize to the scale the domain actually has. Record the decision at the boundary rather than deep in business logic, and never let the exactly-converted value flow on as if it were an exact amount.
  • Does the current decimal context's precision apply when you call the Decimal constructor?
    No. The constructor is exact and ignores prec, so Decimal("1.23456789") keeps all nine digits even under a context whose precision is four. Only arithmetic operations round to the context precision. If you want construction itself to obey the context, call the context object's create_decimal method instead of the constructor.
  • Is storing money as an integer number of cents a valid alternative to using Decimal?
    Yes, and it is common. Integers are exact and fast, and the arithmetic is obvious. The cost is that scale lives in your head rather than in the value: you must remember which fields are minor units, handle currencies with a different number of decimal places, and hand-roll rounding at every division. Decimal keeps the scale and the rounding rule in the object.

Photocopying a smudged page at very high resolution does not remove the smudge; it records it in perfect detail. Decimal(0.1) is that photocopy, and Decimal("0.1") is retyping the page.

saying these in an interview costs you the question

  • Says Decimal(0.1) cleans up the float
  • Believes floats are fine for money if you round at the end
  • Cannot explain why binary cannot hold one tenth
  • Thinks Decimal and float differ only in precision digits
  • Parses incoming JSON amounts as floats then converts
  • Claims the constructor rounds to the context precision

context

open as a page

What does calling random.seed(42) do to the sequence returned by random.random()?

level: juniorimportance: must knowfreq 62%

basics

~20 s

It resets the internal state of the one Mersenne Twister generator that backs the random module's functions. Any run that seeds with the same value then replays the identical sequence of numbers in the same order.

open as a page

How does decimal.Decimal.quantize round a money amount, and what rounding does it use by default?

level: middleimportance: must knowfreq 48%

basics

~10 s

quantize takes a pattern Decimal such as Decimal("0.01") and returns a value with that same exponent, rounding the extra digits away. The rounding rule comes from the current decimal context, whose default is ROUND_HALF_EVEN.

open as a page

How do you choose between statistics.mean, median, stdev and pstdev in Python?

level: juniorimportance: should knowfreq 32%

basics

~20 s

statistics.mean averages every value, so one outlier drags it; statistics.median returns the middle value and ignores how extreme the tail is. statistics.stdev divides by n-1 for a sample, statistics.pstdev by n for a whole population.

open as a page

When does math.fsum give a different total than the built-in sum() over the same floats?

level: middleimportance: should knowfreq 32%

basics

~20 s

math.fsum tracks exact partial sums and rounds once at the end, so its total is always correctly rounded. The built-in sum() has compensated since Python 3.12, but its single correction term can still drift across widely separated magnitudes.

open as a page

Why does float('nan') == float('nan') return False, and how do you detect nan?

level: middleimportance: should knowfreq 46%

basics

~10 s

IEEE 754 defines nan as unordered: every comparison involving it is false, including equality with itself. Detect it with math.isnan(x), or use math.isfinite(x) to reject nan and both infinities in one check.

open as a page

When should you create your own random.Random instance instead of calling the module-level random functions?

level: middleimportance: should knowfreq 45%

basics

~20 s

Whenever the stream must belong to you: library code, anything that seeds for reproducibility, and per-thread draws. The module-level functions share one hidden generator that any other code in the process can reseed or consume.

open as a page

Why does `deck = random.shuffle(deck)` set deck to None, and what returns a shuffled copy?

level: middleimportance: should knowfreq 40%

basics

~10 s

random.shuffle reorders a mutable sequence in place and returns None, following Python's convention for mutating methods. For a shuffled copy, call random.sample(deck, k=len(deck)), which returns a new list and leaves the original untouched.

open as a page

Why does mutating decimal.getcontext().prec in one queue worker change later jobs' totals?

level: seniorimportance: should knowfreq 32%

basics

~20 s

decimal.getcontext() returns shared mutable state, not a per-call setting, so a job that changes prec leaves it changed for everything that runs after it. Scope the change with decimal.localcontext(), which copies the context and restores it on exit.

open as a page

How do you choose math.isclose rel_tol and abs_tol when comparing drifted totals?

level: seniorimportance: should knowfreq 40%

basics

~20 s

math.isclose passes when the difference is at most max(rel_tol * max(abs(a), abs(b)), abs_tol). Size rel_tol from how many operations produced the value, and set abs_tol from the smallest difference that matters, because the default 0.0 makes any comparison against zero fail.

open as a page

Why is output from random.random() predictable, and what does random.SystemRandom change?

level: seniorimportance: should knowfreq 42%

basics

~20 s

The random module runs a Mersenne Twister whose whole state can be reconstructed from a few hundred observed outputs, making past and future values computable. random.SystemRandom draws each value from the operating system's cryptographic source instead, giving up seeding and reproducibility.

open as a page

What does fractions.Fraction.limit_denominator do, and when is Fraction the right numeric type?

level: middleimportance: nice to knowfreq 16%

basics

~10 s

limit_denominator returns the closest Fraction whose denominator is at most the limit you give, so Fraction(0.1).limit_denominator(1000) is 1/10. Fraction stores an exact numerator and denominator, which suits ratios that division must not degrade.

open as a page