Back to Blog
Python

Python Floating Point: Precision, Errors, and Fixes

python floating point: Explains why 0.1 + 0.2 is not 0.3 in Python, how binary floating point works, and how to compare, round, and choose the right numeric type.

floating-pointnumeric-precisiondecimal-moduleieee-754rounding
Editorial thumbnail showing a number line where 0.1 plus 0.2 lands slightly past 0.3, illustrating floating point precision error.

The 0.1 + 0.2 Problem

Run this in any Python interpreter:

print(0.1 + 0.2)

The output is 0.30000000000000004, not 0.3. This is the most common way developers encounter python floating point behavior, and it is not a bug in Python. It is a direct consequence of how the IEEE 754 double-precision format stores numbers.

Python's float type uses 64 bits: one sign bit, 11 exponent bits, and 52 fraction bits with an implicit leading bit, giving 53 bits of precision. The value 0.1 cannot be represented exactly in this binary format because its binary expansion is an infinitely repeating fraction, just as 1/3 cannot be written exactly in decimal. When you write 0.1 in source code, Python stores the nearest representable double, which is slightly larger than the true decimal value. The same happens for 0.2. Adding the two stored approximations produces a result that is slightly off from 0.3.

Why Binary Representation Causes Precision Loss

The core issue is that the base of the representation does not match the base of the input. Decimal literals are parsed as binary fractions, and most decimal fractions do not terminate in binary. Only numbers whose denominator is a power of two, such as 0.5, 0.25, or 0.125, are stored exactly.

This affects more than addition. Multiplication, division, and accumulation all carry the same error. A loop that adds 0.1 ten times does not produce 1.0:

total = 0.0 for _ in range(10): total += 0.1 print(total) # 0.9999999999999999

The error is small relative to the magnitude of the operands, which is why most applications never notice it. But any code that relies on exact equality, exact totals, or exact comparisons will fail at some point.

Comparing Floating Point Values Safely

Direct equality checks against computed floats are unreliable. Instead of x == y, use a tolerance-based comparison. The math module provides isclose, which handles both relative and absolute tolerance:

import math a = 0.1 + 0.2 b = 0.3 math.isclose(a, b) # True math.isclose(a, b, rel_tol=1e-9) # True

rel_tol scales with the magnitude of the larger operand, which is appropriate when the values are large. abs_tol sets a fixed absolute tolerance, useful when the values are close to zero. For values near zero, relative tolerance alone can cause false negatives because the relative error becomes meaningless. A common pattern is to combine both:

math.isclose(a, b, rel_tol=1e-9, abs_tol=1e-12)

When comparing a computed result against an expected constant, choose tolerances based on the number of operations performed. Each operation can introduce an error on the order of one unit in the last place, so a chain of arithmetic needs a looser tolerance than a single operation.

Rounding and Formatting Output

Displaying floats is a separate concern from computing with them. The round function and format specifiers control how a value is rendered, but they do not change the underlying stored value.

round(2.675, 2) # 2.67, not 2.68

This surprises many developers. The stored value of 2.675 is slightly less than the decimal literal, so rounding to two places yields 2.67. The same binary representation issue that affects arithmetic also affects rounding.

For display purposes, use format strings:

value = 0.1 + 0.2 print(f"{value:.2f}") # 0.30

The :.2f specifier rounds the value for display only. If the goal is to produce a fixed number of decimal places for a report or an API response, formatting is the correct tool. If the goal is to perform exact decimal arithmetic, formatting is not enough.

The decimal Module for Exact Decimal Arithmetic

When the input and output are decimal and exactness matters, use the decimal module. It stores numbers as decimal digits with an explicit exponent, so 0.1 is represented exactly.

from decimal import Decimal a = Decimal("0.1") b = Decimal("0.2") print(a + b) # 0.3

The constructor must receive a string. Passing a float passes the already-approximated binary value into the decimal representation, which defeats the purpose:

Decimal(0.1) # Decimal('0.1000000000000000055511151231257827021181583404541015625')

The decimal module uses a configurable context that controls precision, rounding mode, and traps for exceptional conditions. The default precision is 28 significant digits, which is sufficient for most financial calculations. For higher precision, adjust the context:

from decimal import getcontext getcontext().prec = 50

Decimal arithmetic is appropriate for financial calculations, tax amounts, and any domain where the decimal representation is the source of truth. It is not a general replacement for float because it is significantly slower.

Performance and Memory Tradeoffs

The float type is backed by the hardware floating-point unit, so arithmetic operations compile to single machine instructions. The decimal module implements arithmetic in software, which involves more work per operation and uses more memory per value because each Decimal object carries a context and a variable-length significand.

The practical consequence is that float is the right default for scientific computation, graphics, machine learning, and any workload dominated by numeric operations. Decimal is the right choice when the cost of a wrong digit exceeds the cost of slower arithmetic, such as in billing systems. There is no benchmark number that applies universally here; the decision depends on the operation mix and the volume of data.

Choosing Between float, Decimal, Fraction, and int

The standard library offers several numeric types, and the choice depends on what the data represents.

TypeExact representationBest for
floatPowers of two onlyScientific, graphics, ML, general numerics
DecimalAll decimal fractionsFinancial, tax, currency, exact decimal math
FractionAll rational numbersRatios, exact rational arithmetic
intAll integers, arbitrary sizeCounts, indices, money as cents

Fraction stores a numerator and denominator pair, so it can represent 1/3 exactly. It is useful when the problem is inherently rational, such as computing proportions. int is the right choice for money when you track cents or the smallest currency unit as an integer, avoiding floating point entirely.

Edge Cases: NaN, Infinity, and Overflow

Floating point also includes special values that behave differently from ordinary numbers. float('nan') represents a not-a-number result, and float('inf') represents infinity. These values propagate through arithmetic, and comparing them requires dedicated functions:

import math math.isnan(float('nan')) # True math.isinf(float('inf')) # True

NaN does not equal itself, so x == x is False when x is NaN. This makes NaN useful for detecting uninitialized or invalid results, but it also means equality checks and sorting behave unexpectedly. Use math.isnan and math.isinf explicitly when these values are possible.

Overflow occurs when a computation exceeds the largest representable double, which is about 1.8e308. Multiplying two large floats can produce inf silently. Code that processes unbounded input should check for non-finite results before using the value in further calculations.

python floating point: Practical Usage and Code Examples | RYUSLOG DEV