Home› Tutorials› Standard Library
Standard LibraryPython's decimal and fractions Modules: Exact Arithmetic Without Floating-Point Surprises
- Binary floating-point can't represent most decimal fractions exactly, which is why
0.1 + 0.2shows as0.30000000000000004. decimal.Decimalstores numbers in base 10 with a configurable precision, avoiding that whole class of rounding surprise.- Money math should almost always use
Decimal, constructed from strings, never from an already-imprecise float. fractions.Fractionkeeps numbers as an exact numerator/denominator pair, useful when the result needs to stay a precise ratio, not a decimal approximation.
The float problem: 0.1 + 0.2
This surprises almost everyone the first time they see it in Python, or any language using the same IEEE 754 floating-point standard:
print(0.1 + 0.2)
# 0.30000000000000004
Floats are stored in binary, and most decimal fractions — including 0.1 — don't have an exact binary representation, the same way 1/3 has no exact finite decimal representation. The stored value is the closest binary approximation, and small rounding errors like this accumulate through repeated arithmetic. For most numeric work — averages, physics simulations, graphics — this error is far below any meaningful threshold and safely ignorable. For anything counting money, it isn't: adding up a shopping cart in floats can genuinely produce a total off by a fraction of a cent, and depending on how the total is rounded and displayed, that can compound into a real discrepancy.
Decimal basics and context precision
decimal.Decimal stores digits in base 10 instead of binary, so it represents 0.1 exactly, not as an approximation:
from decimal import Decimal
a = Decimal("0.1")
b = Decimal("0.2")
print(a + b) # 0.3 -- exact
Note the string arguments — Decimal("0.1"), not Decimal(0.1). Constructing from the float would first convert your literal to its already-imprecise binary approximation before Decimal ever sees it, defeating the entire point. The module also has a configurable context controlling precision and rounding mode, accessible via decimal.getcontext(), which matters for calculations involving division that wouldn't terminate in a finite number of digits otherwise.
Why Decimal matters for money
Money calculations need two properties floats don't reliably give you: exact representation of the values involved, and predictable rounding behaviour:
from decimal import Decimal, ROUND_HALF_UP
price = Decimal("19.99")
quantity = 3
subtotal = price * quantity
tax = (subtotal * Decimal("0.08")).quantize(Decimal("0.01"), rounding=ROUND_HALF_UP)
print(subtotal, tax) # 59.97 4.80
quantize() rounds to a specified number of decimal places using an explicit rounding mode, rather than leaving rounding behaviour to whatever a float happens to produce. This is the level of control accounting and billing code actually needs — the same reasoning that shows up when validating financial inputs, which often pairs with the custom validation logic covered in the exception handling guide when an amount can't be parsed cleanly.
Fraction basics for exact ratios
fractions.Fraction solves a related but different problem: keeping a value as an exact ratio rather than any decimal approximation at all, decimal or binary:
from fractions import Fraction
a = Fraction(1, 3)
b = Fraction(1, 6)
print(a + b) # 1/2 -- exact, not 0.5 or 0.4999...
print(float(a + b)) # 0.5
This matters for anything where the underlying quantity genuinely is a ratio — a recipe scaling calculator, a music timing library working in fractions of a beat, or symbolic-adjacent math where converting to decimal at all would lose information. Fraction reduces automatically: Fraction(2, 4) stores as 1/2, not 2/4, which keeps comparisons between equivalent fractions working correctly with plain ==.
Converting between float, Decimal, and Fraction
All three types convert to each other, but the direction matters. Going from float to Decimal or Fraction via the constructor preserves the float's exact (if ugly) binary value rather than the decimal number you probably meant:
print(Decimal(0.1)) # Decimal('0.1000000000000000055511151231257827021181583404541015625')
print(Fraction(0.1)) # Fraction(3602879701896397, 36028797018963968)
That's the float's true binary value laid bare — useful for understanding the problem, not useful as an actual input. To get the decimal number you meant, always construct from a string: Decimal("0.1") or Fraction("1/10"). Converting the other way, float(Decimal("0.1")) or float(Fraction(1, 3)), deliberately re-introduces the binary approximation, which is fine once you're done with exact arithmetic and just need a number to print, plot, or pass to a library that only accepts floats. The decimal module documentation covers the full context and rounding-mode API for cases needing more than the basics shown here.