Command Palette

Search for a command to run...

Hectal
PHASE 1Beginner ~27 min· topic 3 of 11

Topic 1.3

Decimals: float and Why 0.1 + 0.2 Isn't 0.3

In one line

A float is a number with a decimal point. Computers store floats in binary, so many everyday decimals such as 0.1 cannot be stored exactly and tiny rounding errors appear. Compare floats with math.isclose, and use decimal or fractions when you need exact answers.

Think of it like this

Writing one third as a decimal. You write 0.3333 and stop, because the 3s never end. Your answer is close, never exact. A computer has the same problem with 0.1, but in binary (base 2) instead of base 10. Just as 1/3 cannot be written exactly with ten-based digits, 1/10 cannot be written exactly with two-based digits.

Words you'll meet

New words in this topic, in plain English. Come back here whenever one feels fuzzy.

float
A number with a decimal point, stored in 64 bits.
IEEE 754
The standard that defines how computers store floating-point numbers.
Rounding error
The tiny difference between a number and the closest value that can be stored.
Tolerance
How close two numbers must be to count as equal.
Banker's rounding
Rounding halves to the nearest even number: 2.5 gives 2, 3.5 gives 4.
inf
Infinity, the result of a number too large to store.
nan
"Not a number", the result of an undefined calculation. It is not equal to anything.
Decimal
A type from the decimal module that stores exact decimal digits.
Fraction
A type from the fractions module that stores an exact ratio of two ints.

Step by step

01The surprise

Python prints the shortest text that gives the same stored number back. 0.1 + 0.2 is stored as a binary value slightly above 0.3, and the shortest text that identifies it is 0.30000000000000004. The value 0.3 on its own is a slightly different binary number, so the comparison is False.

main.pywhole filepython
print(0.1 + 0.2)           # 0.30000000000000004
print(0.1 + 0.2 == 0.3)    # False
print(0.1 * 3)             # 0.30000000000000004

02Looking at the real stored value

An f-string is text with {} slots that Python fills with values (Topic 1.9 explains it). The .20f inside the slot asks for 20 digits after the point. It reveals that the stored 0.1 is not exactly one tenth. It is the closest value that a 64-bit binary float can hold.

Each float has a sign bit, 11 bits for the exponent (how far to slide the point) and 52 bits for the digits. That is what "floating" means: the decimal point floats to where it is needed.

main.pywhole filepython
print(f"{0.1:.20f}")    # 0.10000000000000000555
Looking at the real stored valuediagram
Rendering diagram…

03Compare with isclose

math.isclose(a, b) returns True if the two numbers differ by a tiny relative amount (by default one part in a billion). That is almost always what you mean when you test two calculated floats. You can set your own rel_tol or abs_tol.

main.pywhole filepython
import math
print(math.isclose(0.1 + 0.2, 0.3))    # True

04Rounding

round(2.5) returns 2, and round(3.5) returns 4: when a number is exactly halfway, Python picks the even neighbour. round(3.14159, 2) gives 3.14.

Rounding to decimals can still surprise, because the float you pass in was never exactly the decimal you typed. Printing with a format (f"{x:.2f}") is the usual way to show two decimals; round changes the number.

main.pywhole filepython
print(round(2.5), round(3.5), round(-2.5))    # 2 4 -2
print(round(3.14159, 2))                      # 3.14

05inf and nan

A number bigger than a float can hold becomes inf (infinity). It behaves sensibly: inf > 10 ** 100 is True. nan appears when a calculation has no answer, and it poisons everything it touches. nan == nan is False, a rule of IEEE 754 that Python follows, so you test for it with math.isnan.

main.pywhole filepython
inf = float("inf")
nan = float("nan")
print(1e308 * 10)            # inf
print(nan == nan)            # False
import math
print(math.isnan(nan))       # True

06Exact work: Decimal and Fraction

from decimal import Decimal loads just the Decimal tool from the decimal module, so you can write Decimal(...) without the module name. Create a Decimal from text, not from a float: Decimal("0.1") is exactly 0.1, but Decimal(0.1) carries the float's long error digits. Decimal("0.1") + Decimal("0.2") is exactly 0.3.

A Fraction keeps a numerator and a denominator, so Fraction(1, 3) + Fraction(1, 6) is exactly 1/2. Decimal refuses to mix with float: Decimal("0.1") + 0.2 is a TypeError, which protects you from accidentally bringing the error back. Fraction is more relaxed: Fraction(1, 3) + 0.5 works, but the answer is a plain float, so the exactness is lost without a warning. Both mix freely with int.

main.pywhole filepython
from decimal import Decimal
from fractions import Fraction

print(Decimal("0.1") + Decimal("0.2"))      # 0.3
print(Fraction(1, 3) + Fraction(1, 6))      # 1/2

Try it yourself

  1. 1

    Find another surprise

    Print 0.1 + 0.7 and 1.1 * 3. Predict first whether they look clean. Then repeat with Decimal("0.1") + Decimal("0.7").

  2. 2

    Pick the right tool

    Add Decimal("0.10") to itself ten times by writing Decimal("0.10") * 10. Compare the result with 0.1 * 10. Both look fine here, but the Decimal one is exact by construction.

  3. 3

    Banker's rounding

    Print round(0.5), round(1.5), round(2.5) and round(3.5). Notice the pattern of the answers: halves go to the even neighbour.

Code & diagrams

The 0.1 + 0.2 surprise and its cures New tab

Look at line 4: the stored value of 0.1 is not exactly 0.1.

Sign in to run this example in your browser.

Expected output

0.30000000000000004
False
0.30000000000000004
0.10000000000000000555
True
2 4 -2
3.14
1000.0 0.0015 1.1805916207174113e+21
Exact math, infinity and nan New tab

Decimal("0.1") (from text) is exact; Decimal(0.1) (from a float) shows the hidden error.

Sign in to run this example in your browser.

Expected output

0.3
True
59.97
0.1000000000000000055511151231257827021181583404541015625
1/2
True
inf -inf True
inf
False True
A shop bill: float, Decimal and whole paise New tab

1000 items at 0.10 each. The float total is off, Decimal and whole paise are exact. The last lines show why round(2.675, 2) gives 2.67 while Decimal gives 2.68, and why isclose needs abs_tol near zero.

Sign in to run this example in your browser.

Expected output

99.9999999999986 False True
100.00 True
100 0
3.60 23.59
2.67 2.68 0.12
False True

Break it on purpose

Errors are the best teachers. Make each change, read the error, guess what went wrong, then reveal the answer.

Break #1

Convert text that is not a number

Turn the text "abc" into a float.

main.pywhole filepython
price = float("abc")
terminal
$ python main.py
── what you'll see ──
Traceback (most recent call last):
File "main.py", line 1, in <module>
price = float("abc")
ValueError: could not convert string to float: 'abc'

Break #2

Mix Decimal and float

Add a float to a Decimal.

main.pywhole filepython
from decimal import Decimal

total = Decimal("0.1") + 0.2
terminal
$ python main.py
── what you'll see ──
Traceback (most recent call last):
File "main.py", line 3, in <module>
total = Decimal("0.1") + 0.2
~~~~~~~~~~~~~~~^~~~~
TypeError: unsupported operand type(s) for +: 'decimal.Decimal' and 'float'

Myth vs fact

Myth

Python has a bug because 0.1 + 0.2 is not 0.3.

Fact

Every language that uses IEEE 754 doubles (almost all of them) gives this answer. The numbers are stored in binary, where 0.1 is a never-ending fraction.

Myth

Use round() to make float comparisons safe.

Fact

Rounding moves the problem instead of solving it. Compare with math.isclose, or use Decimal or whole numbers.

Myth

nan is just another number, so nan == nan should be True.

Fact

nan means "no valid number". Two unknowns are not known to be equal, so IEEE 754 says nan == nan is False. Use math.isnan.

Myth

Floats are only a little inexact, so they are fine for money.

Fact

Small errors add up over many operations and cause cents to go missing. Use Decimal or integer cents for money.

Pro corner

Extra depth for experienced readers. New to this? Skip it for now and come back later.

  • ▸

    A Python float is a C double: 1 sign bit, 11 exponent bits, 52 fraction bits. Integers up to 2 ** 53 are stored exactly; beyond that, neighbouring floats are more than 1 apart. float.hex() and float.as_integer_ratio() show the exact stored value.

  • ▸

    Machine learning uses smaller floats on purpose: 32-bit (float32), 16-bit (float16, bfloat16) and even 8-bit formats, to make models smaller and faster. The same rounding ideas apply, with less precision. NumPy (Phase 14) lets you pick the size.

  • ▸

    Summing floats loses precision as the errors pile up. math.fsum tracks the lost bits and returns the correctly rounded sum. Since Python 3.12 the built-in sum of floats also uses compensated (Neumaier) summation, so sum([0.1] * 10) is 1.0 on 3.12 and later but 0.9999999999999999 on 3.11 and earlier. Do not rely on that for exactness: for money or accounting, build Decimal values from text at the edge of the program and keep them there, or store whole cents as ints.

  • ▸

    Decimal uses a context: 28 significant digits by default and rounding mode ROUND_HALF_EVEN. Both can be changed (decimal.getcontext() or localcontext()). Rounding to cents is done with quantize: Decimal("2.675").quantize(Decimal("0.01")) is 2.68, whereas round(2.675, 2) gives 2.67 (the float 2.675 is really 2.67499999...). The same trap shows in f"{2.675:.2f}": format specs round the exact stored value, so f"{0.5:.0f}" is 0 and f"{2.5:.0f}" is 2 (ties go to even). Decimal is slower than float but exact in base 10, and is the right tool for invoices and tax.

  • ▸

    isclose(a, b) is relative: with the default rel_tol=1e-09 it can never say a tiny number is close to exactly 0, so math.isclose(1e-10, 0) is False. When comparing against zero pass abs_tol. Two more edge cases an interviewer may probe: a float that overflows by arithmetic becomes inf (1e308 * 10), but 2.0 ** 1024 and 10 ** 400 * 1.0 raise OverflowError instead. And nan also breaks sorting and max, because comparisons with it are all False; and nan in [nan] is True only because list membership checks identity first.

Remember this

  1. 1

    A float (floating-point number) is Python's type for numbers with a fractional part: 3.14, -0.5, 2.0. It is stored in 64 bits (a bit is one tiny 0-or-1 switch) using the IEEE 754 standard, which is the same format used by almost every language and processor. It keeps about 15 to 17 significant decimal digits.

  2. 2

    Because of binary storage, 0.1 + 0.2 gives 0.30000000000000004, and 0.1 + 0.2 == 0.3 is False. This is not a Python bug. Python is faithfully showing the nearest binary values. JavaScript, Java and C give the same result.

  3. 3

    Never compare floats with == after calculations. Use math.isclose(a, b), which says whether two numbers are equal within a small tolerance. (math is a module: a ready-made toolbox that you load with import; Phase 6 teaches modules.) For money, do not use floats: use decimal.Decimal, which stores digits in base 10, or keep whole cents as ints.

  4. 4

    round(x) rounds to the nearest whole number, but halves go to the nearest even number: round(2.5) is 2 and round(3.5) is 4. This is called banker's rounding and it avoids always rounding upwards, which would bias large sums. round(x, 2) rounds to 2 decimals, and is itself subject to float surprises.

  5. 5

    Floats have special values. float("inf") is infinity, and a result too large to store (like 1e308 * 10, where 1e308 is a short way to write 1 followed by 308 zeros) becomes inf. nan means "not a number" (for example inf - inf), and it is never equal to anything, including itself. Test for it with math.isnan().

  6. 6

    The fractions module keeps exact ratios: Fraction(1, 3) + Fraction(1, 6) is exactly 1/2. Use Decimal for exact decimal digits (money, prices) and Fraction for exact ratios. Use plain float for science, graphics and machine learning, where speed matters and tiny errors are fine.

Explain it without notes

01

Why does 0.1 + 0.2 == 0.3 return False?

02

How should you compare two floats in Python, and why?

03

What is banker's rounding? What do round(2.5) and round(3.5) return?

04

When would you choose Decimal or Fraction instead of float?

Practice

01

Print whether 0.1 + 0.2 equals 0.3, and then whether they are close according to math.isclose.

02

Use Decimal to add the prices 19.99, 5.01 and 0.10 exactly, and print the total.

03

Print round(0.5), round(1.5) and round(2.5) and say why the results are 0, 2 and 2.

Trade-offs

  • ↔

    float vs. Decimal. Floats are fast and fit hardware, but are inexact in base 10. Decimals are exact in base 10 but slower (the C-accelerated decimal is still many times slower than float) and refuse to mix with floats, so you must convert deliberately. Integer cents are the cheapest exact option when you only add, subtract and multiply by whole numbers.

  • ↔

    Tolerance vs. exactness. isclose is forgiving, but you must pick a tolerance that fits your data. A tolerance that is too loose hides real differences; too tight and rounding errors fail the test.

Done when you can

  • Done when you can explain in two sentences why 0.1 + 0.2 is not exactly 0.3.

  • Done when you can compare two floats with math.isclose.

  • Done when you can predict round(2.5) and round(3.5).

  • Done when you can create a Decimal the safe way (from text) and say why.

  • Done when you can explain what inf and nan are and why nan == nan is False.