skip to content

Numbers and Booleans

Python's numeric types and the trap inside each: ints that never overflow, floats that cannot hold 0.1, and the exact Decimal and Fraction types you use for money. Precision bugs are cheap to ask about.

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

questions

25

Why use decimal.Decimal instead of float for currency amounts in Python?

level: juniorimportance: must knowfreq 62%

answer

  1. Money is base ten, floats are base two
  2. Some decimal amounts have no binary form
  3. Digits and an exponent, stored in ten
  4. Built from a string, exact and scaled
  5. Mixing with float raises TypeError

basics

~20 s

A 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 s

Money 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 lines
python
from 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.97

go deeper

for a junior

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.

for a middle

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.

for a senior

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.

for a principal

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

context

open as a page

Why does 0.1 + 0.2 == 0.3 evaluate to False in Python?

level: juniorimportance: must knowfreq 85%

basics

~20 s

Python's float is IEEE-754 binary64, which stores exactly only fractions with a power-of-two denominator. 0.1, 0.2 and 0.3 each become nearby approximations, and the sum of the first two lands one step above the stored 0.3.

open as a page

Why can a Python int hold 2 ** 200 without overflowing, and what does that cost?

level: juniorimportance: must knowfreq 60%

basics

~20 s

Python 3 has one integer type and it is arbitrary precision: CPython stores a sign plus a variable-length array of digits and grows it on demand, so arithmetic never wraps. You pay in memory and in math that is slower than machine words.

open as a page

What do -7 // 2 and -7 % 2 evaluate to in Python, and why?

level: juniorimportance: must knowfreq 62%

basics

~20 s

-7 // 2 is -4 and -7 % 2 is 1. Python floors the quotient toward negative infinity instead of truncating toward zero, so the remainder always carries the sign of the divisor rather than the dividend.

open as a page

Why does int('3.0') raise ValueError while int(3.0) returns 3?

level: juniorimportance: must knowfreq 70%

basics

~20 s

int() parses text with the integer grammar only, so the dot in '3.0' makes it a ValueError. Given a float object it converts numerically, truncating toward zero. For decimal text, call float() first and then int().

open as a page

Why does Python's round(2.5) return 2 while round(3.5) returns 4?

level: juniorimportance: must knowfreq 65%

basics

~20 s

Python's built-in round() breaks an exact tie toward the nearest even value, so 2.5 goes down to 2 and 3.5 goes up to 4. This round-half-to-even rule keeps long columns of numbers from drifting upward.

open as a page

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

level: middleimportance: must knowfreq 55%

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.

open as a page

When should you use math.isclose instead of == to compare two floats?

level: middleimportance: must knowfreq 60%

basics

~10 s

Use math.isclose for any value produced by arithmetic, where rounding makes exact equality meaningless. Its default rel_tol of 1e-09 scales with the larger operand, so comparing against zero needs an explicit abs_tol.

open as a page

Why is bool a subclass of int in Python, and where does that bite?

level: middleimportance: must knowfreq 55%

basics

~20 s

bool inherits from int, with True equal to 1 and False equal to 0. So booleans do arithmetic, sum() counts them as flags, isinstance(x, int) accepts them, and True collides with 1 as a dictionary key.

open as a page

Why does round(2.675, 2) return 2.67 in Python rather than 2.68?

level: middleimportance: must knowfreq 55%

basics

~10 s

The literal 2.675 is stored as a binary double slightly below 2.675, so it is not a halfway case at all — the nearest two-decimal value really is 2.67, and round() returns it correctly.

open as a page

Why does float('nan') compare unequal to itself, and what breaks as a result?

level: middleimportance: should knowfreq 45%

basics

~20 s

IEEE-754 defines nan as unordered, so ==, < and > are all False against it and != is True. That silently breaks sorting, min and max, and makes containment depend on whether the same nan object is reused.

open as a page

What invariant ties Python's // and % together, and what does divmod() return?

level: middleimportance: should knowfreq 44%

basics

~20 s

Python guarantees a == (a // b) * b + a % b for a non-zero divisor, so the remainder is fully determined by the floored quotient. divmod(a, b) returns both as one tuple, (a // b, a % b).

open as a page

How does int(s, base) parse text, and why is base 0 special?

level: middleimportance: should knowfreq 45%

basics

~20 s

int(s, base) reads the text as a numeral in that radix, which must be 0 or 2 through 36, with letters serving as digits above 9. Base 0 means infer the radix from a 0b/0o/0x prefix, and it rejects a decimal numeral with leading zeros.

open as a page

How do math.floor, math.ceil and math.trunc differ on a value like -2.5?

level: middleimportance: should knowfreq 45%

basics

~10 s

math.floor goes toward negative infinity, math.ceil toward positive infinity, and math.trunc toward zero. On -2.5 that gives -3, -2 and -2; on 2.5 floor and trunc agree at 2 while ceil gives 3.

open as a page

Why is setting decimal.getcontext().prec globally risky in a long-running service?

level: seniorimportance: should knowfreq 38%

basics

~20 s

getcontext() returns the calling thread's own context, so a precision set at import applies to that thread only - new threads start from the default of 28 digits. It is also ambient state every later operation silently inherits, and prec counts significant digits, not decimal places.

open as a page

With int.to_bytes, what must a binary-index writer pin so another machine decodes the same values?

level: seniorimportance: should knowfreq 38%

basics

~20 s

Pin three things and record them in the format: the byte length, the byteorder, and whether the value is signed. int.from_bytes must use the identical three. A value too large for the length raises OverflowError; a mismatched byteorder corrupts silently.

open as a page

Why does total // size undercount batches in an importer, and what is the correct ceiling-division idiom?

level: seniorimportance: should knowfreq 34%

basics

~20 s

Floor division discards the partial final batch, so 10001 records at 500 per batch reports 20 instead of 21. Compute the ceiling with -(-total // size) or (total + size - 1) // size, both exact for Python ints.

open as a page

Why does int() refuse a 100,000-digit decimal string, and how is that limit changed?

level: seniorimportance: should knowfreq 25%

basics

~20 s

Since Python 3.11 CPython caps int-to-text and text-to-int conversion in decimal at 4300 digits and raises ValueError beyond it, because that conversion is superlinear and makes a cheap denial of service. Change it with sys.set_int_max_str_digits, -X int_max_str_digits or PYTHONINTMAXSTRDIGITS.

open as a page

How do you round money in Python so a payment reconciliation job matches the ledger to the cent?

level: seniorimportance: should knowfreq 40%

basics

~10 s

Stop rounding floats. Parse each amount into a decimal.Decimal from its original string, then quantize to Decimal('0.01') with an explicitly named rounding mode such as decimal.ROUND_HALF_UP, and round once at a defined boundary.

open as a page

How do you choose between decimal.Decimal, int minor units and float for money?

level: principalimportance: should knowfreq 40%

basics

~20 s

Rule 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.

open as a page

What do the 0b, 0o and 0x prefixes and underscores mean in Python numeric literals?

level: juniorimportance: nice to knowfreq 30%

basics

~20 s

0b1010, 0o17 and 0x1f are binary, octal and hexadecimal ways of writing an ordinary int. Underscores such as 1_000_000 are visual digit separators with no runtime effect. A plain decimal literal may not start with a zero: 010 is a SyntaxError.

open as a page

When is fractions.Fraction a better choice than decimal.Decimal?

level: middleimportance: nice to knowfreq 22%

basics

~20 s

Use fractions.Fraction when the quantities are ratios and every operation must stay exact: it holds a numerator and denominator as integers, so one third is exact. decimal.Decimal is base-10 with a digit budget, so one third rounds.

open as a page

What does ~n return for a Python int, and how do you get fixed-width bitwise results?

level: middleimportance: nice to knowfreq 22%

basics

~20 s

In Python, ~n is -n - 1, so ~5 is -6 and never 250. Ints behave as two's complement with an infinite run of sign bits, so mask with & ((1 << width) - 1) whenever you need a fixed-width answer.

open as a page

When does math.fmod(x, y) disagree with x % y in Python, and which should you use?

level: middleimportance: nice to knowfreq 18%

basics

~20 s

They disagree whenever the operands have different signs: math.fmod follows C and gives the dividend's sign, while % floors and gives the divisor's sign. Use % for integers and wrap-around indexing, math.fmod for float remainders.

open as a page

Why does converting a 19-digit int to a Python float make distinct IDs collide?

level: seniorimportance: nice to knowfreq 25%

basics

~10 s

A float keeps only 53 significand bits, so integers above 2**53 round to the nearest representable value. Two 19-digit identifiers can round to the same double, after which they are indistinguishable and compare equal.

open as a page