Python Module 2 – Values, variables, numbers and strings
Floating-point surprises and how to handle them
Why 0.1 + 0.2 is not 0.3 in Python, how floats are stored in binary, and how to compare, add and round them safely with math.isclose, fsum and Decimal.
What you will learn
- Explain why 0.1 + 0.2 != 0.3 using binary representation
- Compare floats safely with math.isclose
- Choose between float, decimal.Decimal and fractions.Fraction
Before you start
On this page
Ask Python for 0.1 + 0.2 and it answers 0.30000000000000004. Nothing is broken: C, Java and almost every other
language store such numbers the same way and get the same result. Once you know what a float really holds, the
surprises become predictable, and a few habits keep them out of your results.
What a float really stores
On almost every computer, a Python float is an IEEE 754 “double precision” number: a binary fraction with 53
significant bits and an exponent. Binary digits after the point stand for halves, quarters, eighths and so on, so a
fraction can be stored exactly only if it is a sum of such pieces, which means its denominator, in lowest terms, is a
power of 2. 0.75 is ½ + ¼ and is exact. 0.1 is 1/10, and 10 has a factor of 5, so in binary it never ends:
0.000110011001100… The float keeps the first 53 significant bits and rounds the rest away.
from decimal import Decimal
print(0.1 + 0.2)
print(0.1 + 0.2 == 0.3)
print(0.5 + 0.25 == 0.75) # halves and quarters are exact in binary
print(Decimal(0.1)) # the exact value stored for 0.1
print(Decimal(0.2))
print(Decimal(0.3))
print((0.1).as_integer_ratio()) # the same stored value as a fraction
print(2 ** 55) Output
0.30000000000000004 False True 0.1000000000000000055511151231257827021181583404541015625 0.200000000000000011102230246251565404236316680908203125 0.299999999999999988897769753748434595763683319091796875 (3602879701896397, 36028797018963968) 36028797018963968
Recorded with Python 3.14.8 on macOS 26 arm64. To run it yourself: mise exec python@3.14.8 -- python3 stored_value.py
Runs on this device, in your browser. The first run downloads Python (about 13.5 MB), which is kept for the next runs.
Your run, in this browser
Converting a float to Decimal shows every digit of the value it holds. The float for 0.1 is a little more than
0.1, the one for 0.2 a little more than 0.2, and the one for 0.3 a little less than 0.3. As a fraction,
as_integer_ratio() shows the stored 0.1 is 3602879701896397 divided by 2 to the power 55. When Python prints a
float it chooses the shortest decimal that would be stored as the same float, which is why print(0.1) shows just
0.1 and the error stays hidden until a calculation pushes it into view.
A closer look: why the sum misses
Between two neighbouring floats there is no other float, so every result has to land on one of them. The sum of the stored 0.1 and 0.2 falls exactly halfway between the two floats nearest 0.3:
import math
from fractions import Fraction
exact_sum = Fraction(0.1) + Fraction(0.2) # the sum of the two stored values, exactly
below = Fraction(0.3) # the float that 0.3 is stored as
above = Fraction(math.nextafter(0.3, 1)) # the next float up
print(below < exact_sum < above)
print(exact_sum - below == above - exact_sum) # exactly halfway
print((0.3).hex(), (0.1 + 0.2).hex()) # the last digits: 3 is odd, 4 is even Output
True True 0x1.3333333333333p-2 0x1.3333333333334p-2
Recorded with Python 3.14.8 on macOS 26 arm64. To run it yourself: mise exec python@3.14.8 -- python3 halfway.py
Runs on this device, in your browser. The first run downloads Python (about 13.5 MB), which is kept for the next runs.
Your run, in this browser
Why 0.1 + 0.2 lands on the float above 0.3
Text description of the diagram
A short stretch of the number line shows two neighbouring floats, the closest ones to 0.3. No float lies between them.
- The left one, 0.29999999999999998889…, is the float that 0.3 is stored as: it is the closest float to 0.3, which lies a little to its right.
- The right one, 0.30000000000000004441…, is the next float up.
- The exact sum of the stored values of 0.1 and 0.2 lies exactly halfway between the two. The addition has to round it to one of them, and a tie goes to the float whose last binary digit is even, which here is the right one.
So 0.1 + 0.2 gives the right-hand float, and 0.3 is stored as the left-hand one: two different floats, which is why 0.1 + 0.2 == 0.3 is False.
On a tie, the hardware rounds to the float whose last binary digit is even, IEEE 754’s default rule and the same
“half to even” rule that round() uses. The hexadecimal forms printed above end in 3 and in 4, and the even one is
the upper float. 0.3 written in your program is stored as the lower one, the float closest to 0.3. Two different
floats, so == says False.
The limits of a float
import sys
print(sys.float_info.max) # the largest float
print(sys.float_info.max * 10) # past it: infinity, and no error
try:
sys.float_info.max ** 2 # ** checks for overflow
except OverflowError:
print("** raised OverflowError")
print(sys.float_info.epsilon) # the gap between 1.0 and the next float up
print(2.0 ** 53 + 1 == 2.0 ** 53) # above 2 ** 53 not every whole number fits
print(float(2 ** 53 + 1)) Output
1.7976931348623157e+308 inf ** raised OverflowError 2.220446049250313e-16 True 9007199254740992.0
Recorded with Python 3.14.8 on macOS 26 arm64. To run it yourself: mise exec python@3.14.8 -- python3 float_limits.py
Runs on this device, in your browser. The first run downloads Python (about 13.5 MB), which is kept for the next runs.
Your run, in this browser
- The largest float is about 1.8 × 10³⁰⁸. Beyond it a multiplication gives
inf(infinity) without an error, while**raisesOverflowError(thetryandexceptlines catch it, as in the integers lesson): Python checks only some float operations for overflow. sys.float_info.epsilon, about 2.2 × 10⁻¹⁶, is the gap between 1.0 and the next float. Every operation can be off by about half that much relative to its result, which is why floats are good for roughly 16 significant digits.- Whole numbers are exact only up to 2⁵³ (9,007,199,254,740,992). Above it the gaps between floats are wider than 1,
so
float(2 ** 53 + 1)comes back as 2⁵³. Python’s ints have no such limit, so keep counts and money in ints when you can.
Comparing floats: use math.isclose()
Because a computed float often carries a tiny rounding error, == is the wrong test for one. Ask instead whether two
values are close enough:
import math
total = 0.1 + 0.2
print(total == 0.3)
print(math.isclose(total, 0.3)) # within a relative tolerance of 1e-09
balance = 100.0 - 99.9 - 0.1 # should be zero
print(balance)
print(math.isclose(balance, 0.0)) # relative tolerance alone never accepts 0
print(math.isclose(balance, 0.0, abs_tol=1e-9)) # add an absolute tolerance Output
False True -5.689893001203927e-15 False True
Recorded with Python 3.14.8 on macOS 26 arm64. To run it yourself: mise exec python@3.14.8 -- python3 compare_floats.py
Runs on this device, in your browser. The first run downloads Python (about 13.5 MB), which is kept for the next runs.
Your run, in this browser
math.isclose(a, b) accepts the two values when their difference is at most rel_tol times the larger of the two
in size; the default rel_tol=1e-09 means about nine matching digits, whatever the size of the numbers. Near zero
that rule fails: no number except 0 is close to 0 relative to its own size, so the balance, which should be zero
but is -5.7 × 10⁻¹⁵, is rejected. Add an absolute tolerance, abs_tol, whenever a value may be zero or nearly so,
and choose it from the problem: for an amount in rupees or dollars, such as this balance, 1e-9 is a ten-millionth
of a paisa or a cent, far below anything a bill can contain.
math.isclose() follows the IEEE 754 rules for special values: infinity is close only to itself, and NaN (below)
is close to nothing, not even to another NaN.
Adding up many floats
Each addition rounds its result, and the small errors can pile up:
import math
total = 0.0
for _ in range(10):
total += 0.1 # ten additions, each one rounded
print(total)
print(sum([0.1] * 10)) # sum() keeps track of the rounding errors
print(math.fsum([0.1] * 10))
values = [1e16, 1e-16, 1.0]
print(sum(values), math.fsum(values)) # here only fsum() gets it right Output
0.9999999999999999 1.0 1.0 1e+16 1.0000000000000002e+16
Recorded with Python 3.14.8 on macOS 26 arm64. To run it yourself: mise exec python@3.14.8 -- python3 adding_up.py
Runs on this device, in your browser. The first run downloads Python (about 13.5 MB), which is kept for the next runs.
Your run, in this browser
The loop rounds after every step and ends just short of 1.0. sum() keeps track of the rounding errors as it goes
(an algorithm called Neumaier summation) and gets exactly 1.0. math.fsum() goes further and tracks several partial
sums, so no precision is lost along the way. The last line shows a case where only fsum() is right: the true total
is just over 10000000000000001, and the nearest float to that is 1.0000000000000002e+16.
Version note
The more accurate sum() of floats is new in Python 3.12. Python 3.11 and older add the floats one by one, so
sum([0.1] * 10) gives 0.9999999999999999 there, the same as the loop.
Rounding surprises
from decimal import Decimal
print(round(2.675, 2)) # 2.67, not 2.68
print(Decimal(2.675)) # the value really stored is a little below 2.675
print(f"{2.675:.2f}") # formatting rounds the stored value too
print(round(0.125, 2)) # 0.125 is exact: a true tie, so half to even
percent = 0.57 * 100
print(percent)
print(int(percent)) # int() drops the fraction: 56
print(round(percent)) # round first: 57 Output
2.67 2.67499999999999982236431605997495353221893310546875 2.67 0.12 56.99999999999999 56 57
Recorded with Python 3.14.8 on macOS 26 arm64. To run it yourself: mise exec python@3.14.8 -- python3 rounding_surprises.py
Runs on this device, in your browser. The first run downloads Python (about 13.5 MB), which is kept for the next runs.
Your run, in this browser
round(2.675, 2) gives 2.67, not 2.68, because the float written as 2.675 is really a little below it, so there is
no tie to round up. Formatting with :.2f rounds the same stored value and agrees. 0.125, on the other hand, is
exactly ⅛, a real tie, and round() sends it to the even neighbour, 0.12.
A common mistake: int() after float arithmetic
0.57 * 100 is 56.99999999999999, and int() drops the fraction, so the result is 56 instead of 57. Percentages,
prices in paise or cents and other “convert to a whole number” steps go wrong this way. Use round() instead,
which also returns an int.
Infinity, NaN and minus zero
import math
inf = float("inf")
nan = float("nan") # "not a number"
print(inf > 10 ** 300, -inf < 0)
print(inf - inf, inf * 0) # no sensible answer: nan
print(nan == nan, nan != nan) # nan is not equal even to itself
print(math.isnan(nan), math.isinf(inf), math.isfinite(1e308))
print(0.0 == -0.0, math.copysign(1, -0.0)) # equal, but the sign is kept Output
True True nan nan False True True True True True -1.0
Recorded with Python 3.14.8 on macOS 26 arm64. To run it yourself: mise exec python@3.14.8 -- python3 special_values.py
Runs on this device, in your browser. The first run downloads Python (about 13.5 MB), which is kept for the next runs.
Your run, in this browser
float("inf")and-float("inf")are larger and smaller than every finite number.nan(“not a number”) is the result of operations with no sensible answer, such as infinity minus infinity. Every comparison with it isFalse, evennan == nan; only!=isTrue. Test for it withmath.isnan(), and for infinity withmath.isinf(), or for “an ordinary number” withmath.isfinite().-0.0equals0.0, but it keeps its sign, whichmath.copysign()can read.
When a float is the wrong type
The standard library has two other number types for work where binary rounding is not acceptable. A later lesson covers both in depth; this is when to reach for each:
from decimal import Decimal
from fractions import Fraction
print(Decimal("0.1") + Decimal("0.2")) # decimal arithmetic, from strings
print(Decimal("0.1") + Decimal("0.2") == Decimal("0.3"))
print(Decimal(0.1)) # from a float: the float's error comes along
print(Fraction(1, 3) + Fraction(1, 6)) # exact ratios
print(Fraction(1, 3) * 3 == 1)
print(float.from_number(Fraction(1, 4))) # new in 3.14: numbers only
try:
float.from_number("3.5") # float("3.5") would parse the text
except TypeError as error:
print("TypeError:", error) Output
0.3 True 0.1000000000000000055511151231257827021181583404541015625 1/2 True 0.25 TypeError: must be real number, not str
Recorded with Python 3.14.8 on macOS 26 arm64. To run it yourself: mise exec python@3.14.8 -- python3 exact_types.py
Runs on this device, in your browser. The first run downloads Python (about 13.5 MB), which is kept for the next runs.
Your run, in this browser
| Criterion | float | decimal.Decimal | fractions.Fraction |
|---|---|---|---|
| Stores | a binary fraction with 53 significant bits | decimal digits: 28 significant digits by default, adjustable | an exact numerator and denominator |
| Exact for | halves, quarters, eighths … and whole numbers up to 2⁵³ | decimal amounts such as 0.1 and 19.99 | every ratio of two integers, such as 1/3 |
| Arithmetic | the processor's floating-point hardware | software | software, with numerators and denominators that can grow large |
| When to choose | Measurements, science, graphics and most everyday calculations | Money, and anything that must round the way people do with decimals | Exact ratios, such as probabilities or recipe scaling |
Build a Decimal from a string, Decimal("0.1"), or from an int. Decimal(0.1) copies the float exactly, error and
all, as the third line of output shows.
Version note
New in Python 3.14: float.from_number() converts a number (an int, a Fraction, a Decimal …) to a float and
raises TypeError for anything else, including text. float() still parses strings such as "3.5", so
from_number() is the stricter choice when a value must already be a number.
Key takeaways
- Floats are binary fractions with 53 significant bits; most decimal fractions, including 0.1, are stored as the nearest such value, so small errors are normal.
- Never compare computed floats with
==. Usemath.isclose(), with anabs_tolwhenever values can be near zero. sum()adds floats accurately since Python 3.12;math.fsum()is the most accurate.round(2.675, 2)is 2.67 because of the stored value; useround(), notint(), when you turn a float result into a whole number.- NaN is not equal to itself: test it with
math.isnan(). - Use
Decimal, built from strings, for money, andFractionfor exact ratios; keep floats for measurements and science.
Exercise
Exercise · Easy · Python
Compare floats with a tolerance
Write two functions in compare.py.
The first, almost_equal(a, b), returns True when the floats a and b are equal apart from rounding errors, and False otherwise. Use math.isclose() with its default relative tolerance (rel_tol=1e-09, about nine matching digits) and an absolute tolerance of ABS_TOL, which the starter code sets to 1e-12, so that results near zero also count as close.
The second, surprises(pairs), takes a list of (a, b) pairs and returns, in their original order, the pairs that == calls different but almost_equal() calls equal: the comparisons that == would get wrong.
almost_equal(0.1 + 0.2, 0.3)isTruealmost_equal(1e-15, 0.0)isTruealmost_equal(1e-9, 0.0)isFalsealmost_equal(float("nan"), float("nan"))isFalsesurprises([(0.1 + 0.2, 0.3), (1.0, 1.0), (2.0, 3.0)])is[(0.30000000000000004, 0.3)]
The sample tests compare twelve pairs, among them infinities, NaN, 0.0 and -0.0, and values very close to zero.
Starter code · compare.py
import math
ABS_TOL = 1e-12 # how far apart two numbers near zero may be and still count as equal
def almost_equal(a, b):
"""Return True when a and b are equal within rel_tol=1e-09 or abs_tol=ABS_TOL."""
# Replace this line with your code.
return a == b
def surprises(pairs):
"""Return the pairs (a, b) that == calls different but almost_equal() calls equal, in order."""
# Replace this line with your code.
return [] The sample tests · test_compare.py
from compare import almost_equal, surprises
nan = float("nan")
inf = float("inf")
def test_rounding_errors():
"""treats numbers as equal when they differ only by a tiny fraction of their size"""
assert almost_equal(0.1 + 0.2, 0.3) is True
assert almost_equal(1.1 + 2.2, 3.3) is True
assert almost_equal(1e20, 1e20 + 1e5) is True
def test_real_differences():
"""keeps numbers that really differ apart"""
assert almost_equal(1.0, 1.001) is False
assert almost_equal(100.0, 100.5) is False
def test_relative_tolerance():
"""allows a relative difference of 1e-09, and no more"""
assert almost_equal(1.0, 1.0 + 5e-10) is True
assert almost_equal(1.0, 1.0 + 2e-9) is False
def test_near_zero():
"""allows an absolute difference of 1e-12 near zero, and no more"""
assert almost_equal(1e-15, 0.0) is True
assert almost_equal(100.0 - 99.9 - 0.1, 0.0) is True
assert almost_equal(2e-12, 0.0) is False
assert almost_equal(1e-9, 0.0) is False
def test_signed_zero():
"""treats 0.0 and -0.0 as equal"""
assert almost_equal(0.0, -0.0) is True
def test_special_values():
"""follows the IEEE 754 rules for infinity and NaN"""
assert almost_equal(inf, inf) is True
assert almost_equal(inf, -inf) is False
assert almost_equal(nan, nan) is False
assert almost_equal(nan, 1.0) is False
def test_surprises():
"""finds the pairs that == gets wrong, in their original order"""
pairs = [
(0.1 + 0.2, 0.3),
(1.0, 1.0),
(1e-15, 0.0),
(1e-9, 0.0),
(0.0, -0.0),
(nan, nan),
(inf, inf),
(inf, -inf),
(1e20, 1e20 + 1e5),
(1.0, 1.001),
(1.1 + 2.2, 3.3),
(2.5, 2.5),
]
assert surprises(pairs) == [(0.1 + 0.2, 0.3), (1e-15, 0.0), (1e20, 1e20 + 1e5), (1.1 + 2.2, 3.3)] A hint
math.isclose(a, b, abs_tol=ABS_TOL) already uses rel_tol=1e-09, its default. For surprises(), start with an empty list, go through the pairs with for a, b in pairs:, and append((a, b)) when a != b and almost_equal(a, b) are both true.
Results of the sample tests
| Test | Result | Details |
|---|
What your code printed
The sample tests run on this device, in your browser (Pyodide): nothing is sent to mysmartcopilot.com. The first run downloads Python (about 13.5 MB), which is kept for the next runs. A check in your browser is feedback for you, not proof that the code is right for every input.
Check yourself
5 questions about this lesson. Every answer and why it is right is on the page, behind “Show the answer”. Your score stays in this browser.
References
- The Python Tutorial: floating-point arithmetic, issues and limitations (Python Software Foundation)
- math.isclose() (Python Software Foundation)
- math.fsum() (Python Software Foundation)
- Built-in functions, sum() (Python Software Foundation)
- Built-in functions, round() (Python Software Foundation)
- sys.float_info (Python Software Foundation)
- Value comparisons (not-a-number values) (Python Software Foundation)
- decimal, decimal fixed-point and floating-point arithmetic (Python Software Foundation)
- fractions, rational numbers (Python Software Foundation)
- float.from_number() (Python Software Foundation)
- What's New in Python 3.12: other language changes (Python Software Foundation)
- What's New in Python 3.14: built-ins (Python Software Foundation)
- Built-in exceptions, OverflowError (Python Software Foundation)
- IEEE Standard for Floating-Point Arithmetic (IEEE 754) (IEEE)
Related tools
Report a problem with this lesson
Kept only in this browser. Your Learn progress