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
decimalmodule that stores exact decimal digits. - Fraction
- A type from the
fractionsmodule 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.
print(0.1 + 0.2) # 0.30000000000000004
print(0.1 + 0.2 == 0.3) # False
print(0.1 * 3) # 0.3000000000000000402Looking 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.
print(f"{0.1:.20f}") # 0.1000000000000000055503Compare 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.
import math
print(math.isclose(0.1 + 0.2, 0.3)) # True04Rounding
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.
print(round(2.5), round(3.5), round(-2.5)) # 2 4 -2
print(round(3.14159, 2)) # 3.1405inf 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.
inf = float("inf")
nan = float("nan")
print(1e308 * 10) # inf
print(nan == nan) # False
import math
print(math.isnan(nan)) # True06Exact 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.
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/2Try it yourself
- 1
Find another surprise
Print
0.1 + 0.7and1.1 * 3. Predict first whether they look clean. Then repeat withDecimal("0.1") + Decimal("0.7"). - 2
Pick the right tool
Add
Decimal("0.10")to itself ten times by writingDecimal("0.10") * 10. Compare the result with0.1 * 10. Both look fine here, but the Decimal one is exact by construction. - 3
Banker's rounding
Print
round(0.5),round(1.5),round(2.5)andround(3.5). Notice the pattern of the answers: halves go to the even neighbour.
Code & diagrams
Look at line 4: the stored value of 0.1 is not exactly 0.1.
Expected output
0.30000000000000004
False
0.30000000000000004
0.10000000000000000555
True
2 4 -2
3.14
1000.0 0.0015 1.1805916207174113e+21Decimal("0.1") (from text) is exact; Decimal(0.1) (from a float) shows the hidden error.
Expected output
0.3
True
59.97
0.1000000000000000055511151231257827021181583404541015625
1/2
True
inf -inf True
inf
False True1000 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.
Expected output
99.9999999999986 False True
100.00 True
100 0
3.60 23.59
2.67 2.68 0.12
False TrueBreak 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.
price = float("abc")Break #2
Mix Decimal and float
Add a float to a Decimal.
from decimal import Decimal
total = Decimal("0.1") + 0.2Myth 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
floatis a Cdouble: 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()andfloat.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.fsumtracks the lost bits and returns the correctly rounded sum. Since Python 3.12 the built-insumof floats also uses compensated (Neumaier) summation, sosum([0.1] * 10)is1.0on 3.12 and later but0.9999999999999999on 3.11 and earlier. Do not rely on that for exactness: for money or accounting, buildDecimalvalues from text at the edge of the program and keep them there, or store whole cents as ints. - ▸
Decimaluses a context: 28 significant digits by default and rounding modeROUND_HALF_EVEN. Both can be changed (decimal.getcontext()orlocalcontext()). Rounding to cents is done withquantize:Decimal("2.675").quantize(Decimal("0.01"))is2.68, whereasround(2.675, 2)gives2.67(the float 2.675 is really 2.67499999...). The same trap shows inf"{2.675:.2f}": format specs round the exact stored value, sof"{0.5:.0f}"is0andf"{2.5:.0f}"is2(ties go to even).Decimalis slower than float but exact in base 10, and is the right tool for invoices and tax. - ▸
isclose(a, b)is relative: with the defaultrel_tol=1e-09it can never say a tiny number is close to exactly0, somath.isclose(1e-10, 0)isFalse. When comparing against zero passabs_tol. Two more edge cases an interviewer may probe: a float that overflows by arithmetic becomesinf(1e308 * 10), but2.0 ** 1024and10 ** 400 * 1.0raiseOverflowErrorinstead. Andnanalso breaks sorting andmax, because comparisons with it are allFalse; andnan in [nan]isTrueonly because list membership checks identity first.
Remember this
- 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
Because of binary storage,
0.1 + 0.2gives0.30000000000000004, and0.1 + 0.2 == 0.3isFalse. This is not a Python bug. Python is faithfully showing the nearest binary values. JavaScript, Java and C give the same result. - 3
Never compare floats with
==after calculations. Usemath.isclose(a, b), which says whether two numbers are equal within a small tolerance. (mathis a module: a ready-made toolbox that you load withimport; Phase 6 teaches modules.) For money, do not use floats: usedecimal.Decimal, which stores digits in base 10, or keep whole cents as ints. - 4
round(x)rounds to the nearest whole number, but halves go to the nearest even number:round(2.5)is 2 andround(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
Floats have special values.
float("inf")is infinity, and a result too large to store (like1e308 * 10, where1e308is a short way to write 1 followed by 308 zeros) becomesinf.nanmeans "not a number" (for exampleinf - inf), and it is never equal to anything, including itself. Test for it withmath.isnan(). - 6
The
fractionsmodule keeps exact ratios:Fraction(1, 3) + Fraction(1, 6)is exactly1/2. UseDecimalfor exact decimal digits (money, prices) andFractionfor exact ratios. Use plainfloatfor science, graphics and machine learning, where speed matters and tiny errors are fine.
Explain it without notes
Why does 0.1 + 0.2 == 0.3 return False?
How should you compare two floats in Python, and why?
What is banker's rounding? What do round(2.5) and round(3.5) return?
When would you choose Decimal or Fraction instead of float?
Practice
Print whether 0.1 + 0.2 equals 0.3, and then whether they are close according to math.isclose.
Use Decimal to add the prices 19.99, 5.01 and 0.10 exactly, and print the total.
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
decimalis 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.
iscloseis 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.2is not exactly 0.3.Done when you can compare two floats with
math.isclose.Done when you can predict
round(2.5)andround(3.5).Done when you can create a
Decimalthe safe way (from text) and say why.Done when you can explain what
infandnanare and whynan == nanis False.