skip to content

Why does decimal.Decimal(0.1) differ from decimal.Decimal('0.1')?

level: middleimportance: must knowfreq 55%

answer

  1. Two constructors, two very different inputs
  2. The conversion is faithful, the input was not
  3. Fifty-five digits appear from nowhere
  4. Construction ignores the context precision
  5. Decimal.from_float is the explicit spelling

basics

~10 s

Passing a float converts that float's real binary value exactly, giving Decimal('0.1000000000000000055511151231257827021181583404541015625'). Passing the string parses the digits you wrote, giving exactly one tenth. The error was already in the float literal.

solid answer

~40 s

`Decimal('0.1')` parses a decimal string into the coefficient 1 with exponent -1, which is exactly one tenth. `Decimal(0.1)` takes a binary float and converts it **exactly** - not approximately - and the float nearest to one tenth is 0.1000000000000000055511151231257827021181583404541015625, so that 55-digit value is what you get. Nothing is rounded on the way in: the constructor deliberately ignores the context's precision, because the decimal specification says construction is exact. `Decimal.from_float(0.1)` is the explicit spelling of the same conversion, and `Context.create_decimal` is the variant that *does* apply the context's precision. The takeaway is that the error entered when the literal was written as a float; converting afterwards cannot recover the value you meant, so parse strings at the boundary instead.

code

pycon · 9 lines
pycon
>>> from decimal import Decimal
>>> Decimal(0.1)
Decimal('0.1000000000000000055511151231257827021181583404541015625')
>>> Decimal('0.1')
Decimal('0.1')
>>> Decimal(0.1) == Decimal('0.1')
False
>>> (0.1).as_integer_ratio()
(3602879701896397, 36028797018963968)

go deeper

for a junior

Recall the rule of thumb: build a Decimal from a string or an int, never from a float. Know that Decimal(0.1) shows a long tail of digits and that this reflects the float, not a bug.

for a middle

Explain the mechanism precisely: the float is converted exactly, every binary fraction has a finite decimal expansion, and the constructor ignores context precision because construction is defined as exact. Name Decimal.from_float and Context.create_decimal.

for a senior

Show where floats sneak in - JSON decoding, CSV parsing, database drivers, config - and move the conversion to the boundary so the value is never a float. Be able to argue why Decimal(str(x)) is a patch, not a fix.

for a principal

Make the rule enforceable: one parsing layer that produces exact types, a wire format that carries amounts as strings, and a check that fails review when a float reaches the ledger path. Decide who owns that boundary.

### Two different inputs, two different meanings `decimal.Decimal` accepts several argument types, and the interesting difference is between a string and a float. Given a **string**, the constructor parses the characters. `Decimal('0.1')` becomes a coefficient of 1 with an exponent of -1 - exactly one tenth, with a scale of one decimal place. What you typed is what you get. Given a **float**, the constructor converts the float's actual value with no loss at all. That sounds reassuring until you remember what the float actually holds. `0.1` written in Python source is not one tenth; it is the binary64 value nearest to one tenth. Every binary fraction has a finite decimal expansion, so that value can be written out exactly in base 10, and it takes 55 digits: `0.1000000000000000055511151231257827021181583404541015625`. `Decimal(0.1)` shows you those digits. The conversion is faithful; the *input* was already not one tenth. So `Decimal(0.1) == Decimal('0.1')` is False, and so is `Decimal('0.1') == 0.1`, while `Decimal('0.5') == 0.5` is True - a half is a power-of-two fraction and survives binary exactly. ### Why the constructor does not round A natural objection: the default context allows 28 significant digits, so why does the constructor hand back 55? Because construction is not an arithmetic operation. The decimal specification treats conversion into the type as exact, and CPython follows it: neither `prec` nor the rounding mode is consulted by `Decimal(...)`. `Decimal('123456.789')` stays `123456.789` even under a context with `prec` set to 6. Precision is applied by *operations*. Adding zero, or applying unary plus, runs the value through the context and rounds it: under `prec = 6`, `Decimal('123456.789') + Decimal(0)` gives `123457`. If you want construction itself to respect a context, call that context's own constructor: `Context(prec=3).create_decimal('123456.789')` yields `1.23E+5`. ### The explicit spellings `Decimal.from_float(0.1)` is a classmethod that does exactly what passing the float to the constructor does; it exists to make the intent unmistakable in code that really does want the float's true value - for instance when you are *inspecting* a float rather than computing money with it. The same information is available from `float.as_integer_ratio()`, which returns the exact numerator and denominator, from `float.hex()`, and from `fractions.Fraction(0.1)`, which prints `3602879701896397/36028797018963968`. All four are views of the same underlying fact. ### Where this actually bites The bug in production is almost never someone typing `Decimal(0.1)` in a source file. It is a value that arrives from somewhere as a float and is converted late: a JSON payload decoded with the standard decoder, a CSV column passed through `float()`, a database driver configured to return floats, a configuration value read as a number. By the time your code sees a float, the intended decimal value is gone; wrapping it in `Decimal` preserves the damage with great fidelity rather than repairing it. The fixes all live at the boundary. Decode JSON numbers as strings, or hand the decoder a hook that builds Decimals from the raw text. Read CSV fields as text and construct from the text. Configure the database adapter to return the exact type. Only when none of that is possible do you convert a float, and then you must round explicitly and consciously - and record that you did. ### The seductive wrong fix Candidates often propose `Decimal(str(0.1))`. It happens to work for one tenth, because `repr` of a float prints the shortest string that round-trips to the same float, and for 0.1 that string is '0.1'. But it is not a repair: it is a guess that the shortest round-tripping string is the value somebody originally meant. Feed it the result of a float computation - a value that has already drifted - and it faithfully preserves the drift in a shorter form. It also silently changes the number of significant digits you carry. Use it as a last-resort conversion at a boundary you cannot control, never as the reason it is safe to let floats into money code. ### What to say in the interview One sentence for the mechanism ('the float is converted exactly, and the float is not one tenth'), one for the design reason ('construction is exact by specification, precision applies to operations'), and one for the consequence ('so parse strings at the boundary; a late conversion cannot recover what the literal already lost').

  • Is Decimal(str(value)) a safe way to convert a float you were handed?
    Only as a last resort. `repr` prints the shortest string that round-trips to the same float, so for 0.1 you get '0.1' and it looks like a repair. It is really a guess that the shortest string is the value someone meant. Give it a float that has already drifted through a calculation and it preserves the drift. Fix the boundary so the value never becomes a float instead.
  • Why does the Decimal constructor ignore the context's precision at all?
    The decimal specification defines conversion into the type as exact, so no information is discarded on the way in; precision and rounding are properties of operations. That keeps construction predictable and independent of ambient state. When you do want the context applied at construction time, call `create_decimal` on a Context object, which rounds to that context's precision.
  • How would you keep floats out of amounts arriving as JSON?
    Decode the numbers from their original text rather than after the fact: the standard JSON decoder accepts a hook for parsing float-shaped literals, so you can build a Decimal from the raw token. Better still, agree that the wire format carries amounts as JSON strings and construct `Decimal(payload['amount'])`. Either way the value is never a float, so there is nothing to recover.

Photocopying a smudged receipt at the highest possible resolution gives you a perfect copy of the smudge. Decimal(0.1) is that photocopy; Decimal('0.1') is retyping the number from the original invoice.

saying these in an interview costs you the question

  • Says Decimal(0.1) rounds the float to context precision
  • Believes the extra digits are a decimal module bug
  • Thinks Decimal(str(x)) makes float input safe
  • Expects Decimal('0.1') == 0.1 to be True
  • Assumes the constructor and arithmetic use the same rules

context