Q1Which is the most appropriate choice when you can't tolerate even a one-cent drift in monetary calculations?
decimal and fractions — Precise Arithmetic Without Float Errors
Learn Python's decimal and fractions modules from the ground up. Covers the 0.1 + 0.2 != 0.3 problem with float, exact monetary calculation with Decimal (and why you build Decimals from strings), arithmetic with auto-reducing Fraction, and choosing among the three numeric types — with runnable practice exercises.
This article covers the two modules for the cases where `float` causes problems. decimal.Decimal does exact base-10 arithmetic, which makes it essential for monetary calculations; fractions.Fraction keeps the numerator and denominator as integers so it can compute with reduced fractions exactly. Both exist to remove the inevitable rounding errors of float.
Float Rounding Errors and decimal.Decimal — Don't Use Float for Precise Math
Python's float is represented internally in binary, so decimal fractions like `0.1` that don't divide cleanly in binary carry a tiny rounding error. The most famous example is `0.1 + 0.2 == 0.3` returning `False`. For graphics or scientific computing the error is negligible, but in financial sums even a small drift becomes critical.
That's why decimal.Decimal exists. It's represented internally in base 10, so building one from the string "0.1" lets you compute without error.
0.1. Decimal uses a base-10 internal representation, so it computes exactly as the input string says, but is slower than float. For money and tax processing, choose Decimal.from decimal import Decimal
# float rounding error
print(0.1 + 0.2) # 0.30000000000000004
print(0.1 + 0.2 == 0.3) # False ← against intuition!
# Build Decimal from a string (building from float inherits the float's error)
a = Decimal("0.1")
b = Decimal("0.2")
print(a + b) # 0.3
print(a + b == Decimal("0.3")) # True
# A monetary example
price = Decimal("1980")
tax_rate = Decimal("0.10")
total = price * (Decimal("1") + tax_rate)
print(total) # 2178.00
Build Decimal from a string, not from a float
If you pass a float — Decimal(0.1) — you're handing in a value that already carries a rounding error, so you end up with a Decimal that still has the float's error. Always pass a string: Decimal("0.1"). It's hard to spot in tests and is the classic pitfall that produces a one-cent drift in production for the first time.
fractions.Fraction — Keeping Fractions as Fractions
Fraction is a type that stores the numerator and denominator as integers for arithmetic. Build one with Fraction(1, 3), and subsequent additions, subtractions, and multiplications return a reduced-fraction result. As a float, 1/3 is the approximation 0.3333333333333333, but with Fraction it stays as "one-third" itself.
+ / - / * are also Fraction, and they're automatically reduced. You can convert to float with float(f), but that introduces rounding error.| Type | Internal representation | Best fit |
|---|---|---|
| int | Integer | Counts and tallies (no error) |
| float | Binary floating-point | Scientific computing, graphics (fast, with error) |
| Decimal | Base-10 representation | Money and tax (when base-10 precision matters) |
| Fraction | Numerator and denominator (integers) | Ratios and probabilities (when you want a reduced form) |
Fraction objects expose attributes for the numerator and denominator. f.numerator returns the numerator and f.denominator returns the denominator — both after reduction. For example, Fraction(2, 6) is normalized internally to Fraction(1, 3), so .numerator is 1 and .denominator is 3.
| Attribute / Method | Meaning | Example |
|---|---|---|
| f.numerator | Numerator | Fraction(1, 3).numerator → 1 |
| f.denominator | Denominator | Fraction(1, 3).denominator → 3 |
| float(f) | Convert to float | float(Fraction(1, 2)) → 0.5 |
Knowledge Check
Answer each question one by one.
Q2When constructing a Decimal, which is the correct way that doesn't introduce error?
Q3Which one do you reach for when you want to compute ratios or probabilities as reduced fractions?