Floating point¶
This is the notebook that explains the single most reported "bug" in every programming language: 0.1 + 0.2 doesn't equal 0.3. It isn't a bug, and it isn't specific to Python — it's how binary floating point works everywhere. Once you understand why, the practical rules (don't compare floats with ==, use Decimal for money) stop feeling like superstition.
The famous example¶
print(0.1 + 0.2) # 0.30000000000000004
print(0.1 + 0.2 == 0.3) # False
That trailing ...04 is real, and == sees it. So the equality is genuinely false. To understand where it comes from, look at what 0.1 actually is once stored.
Why: decimal fractions don't fit in binary¶
A float stores numbers in binary fractions — sums of halves, quarters, eighths, and so on. Some decimals land exactly on such a sum (0.5 is 1/2, 0.25 is 1/4). Most don't. 0.1 is a repeating fraction in binary, just as 1/3 is the repeating 0.333... in decimal — so it gets stored as the nearest representable value, which is very slightly off.
Ask Python to print 0.1 to 17 decimal places and the discrepancy appears:
print(format(0.1, '.17f')) # 0.10000000000000001 — not exactly 0.1
print(format(0.3, '.17f')) # 0.29999999999999999
print(format(0.1 + 0.2, '.17f')) # 0.30000000000000004
The normal print(0.1) shows 0.1 because Python rounds to the shortest string that round-trips back to the same float. The error is still there; it's just hidden until an operation (like adding) makes it big enough to surface in the short form.
What a float can represent¶
A Python float is an IEEE 754 double: 64 bits, giving about 15–17 significant decimal digits of precision and a range up to roughly 1.8 × 10³⁰⁸. That's plenty for measurements, physics, graphics, and statistics. What it can't do is represent most decimal fractions exactly — which is exactly what money needs. The how floats work essay unpacks the bit layout; the practical upshot is the precision figure.
import sys
print(sys.float_info.dig) # 15 — guaranteed significant decimal digits
print(sys.float_info.max) # ~1.7976931348623157e+308
Comparing floats: never ==¶
Because results carry tiny errors, testing two floats for exact equality is unreliable. Instead ask whether they're close enough, with math.isclose. It handles the tolerance for you (relative by default, with an optional absolute tolerance for values near zero).
import math
print(0.1 + 0.2 == 0.3) # False — don't do this
print(math.isclose(0.1 + 0.2, 0.3)) # True — do this
# near zero, give an absolute tolerance too:
print(math.isclose(1e-10, 0.0)) # False (relative tol fails at 0)
print(math.isclose(1e-10, 0.0, abs_tol=1e-9)) # True
The surprise in round¶
round uses banker's rounding (round half to even): a value exactly halfway goes to the nearest even digit, not always up. So round(0.5) is 0, round(2.5) is 2, but round(3.5) is 4. This reduces statistical bias when you round lots of numbers, but it catches everyone the first time.
print(round(0.5), round(1.5), round(2.5), round(3.5)) # 0 2 2 4
# And rounding to decimal places can surprise you because of representation:
print(round(2.675, 2)) # 2.67, not 2.68 — 2.675 is stored as 2.6749999...
If you need the "always round half up" rule most people expect — especially for money — round is the wrong tool. Use Decimal with an explicit rounding mode, covered in the rounding recipe and the Decimal notebook.
Infinity and not-a-number¶
Floats include two special values. Infinity (inf) is what you get from overflow or an explicit float('inf'). Not-a-Number (nan) represents an undefined result like inf - inf or 0/0-style operations. Both propagate through arithmetic.
import math
print(math.inf, -math.inf) # inf -inf
print(1e308 * 10) # inf — overflow, not an error
print(math.inf - math.inf) # nan — undefined
print(float('nan')) # nan
nan has a property that trips people up: it is not equal to anything, including itself. So you can't test for it with ==; use math.isnan. (This is by design — it's how IEEE 754 lets a nan signal "no valid answer".)
nan = float('nan')
print(nan == nan) # False (!)
print(nan == 0, nan < 1) # False False — all comparisons are False
print(math.isnan(nan)) # True — the correct way to test
print(math.isinf(math.inf)) # True
A nasty consequence: a nan hiding in a list breaks sorting and min/max in confusing ways, and nan in [nan] can be True (because membership tests identity first). If your data might contain nan, filter it out early with math.isnan.
So when is float the right choice?¶
Most of the time. Use float for anything measured or continuous — physical quantities, coordinates, percentages, scientific computation, statistics — where a relative error around the 16th digit is irrelevant. Avoid it when values must be exact: money, and anything where rounding has to follow a specific published rule. For those, reach for Decimal (next notebook).
Recap¶
floatstores binary fractions, so most decimals (like0.1) are tiny approximations — this is IEEE 754, not a Python quirk.- A double gives ~15–17 significant digits and a huge range.
- Compare with
math.isclose, never==. rounddoes banker's rounding (half to even);round(2.5) == 2.infcomes from overflow;nanis never equal to anything — test withmath.isnan.- Use
floatfor measured quantities; useDecimalwhen exactness matters.
Next: Decimal and Fraction — the exact types, and the one rule that makes Decimal actually work.