Decimal and Fraction¶
The last notebook showed that float can't represent most decimals exactly. When exactness matters — money, above all — Python's standard library has two types that don't approximate: Decimal for exact decimal arithmetic, and Fraction for exact rational numbers. This notebook covers both, and the one rule that makes Decimal actually deliver on its promise.
Decimal: exact decimal arithmetic¶
Decimal represents numbers in base 10, the way you'd write them on paper, so 0.1 really is one tenth. The famous failing sum just works:
from decimal import Decimal
print(Decimal('0.1') + Decimal('0.2')) # 0.3 — exactly
print(Decimal('0.1') + Decimal('0.2') == Decimal('0.3')) # True
The one rule: build from strings, not floats¶
This is the mistake that quietly defeats Decimal. If you write Decimal(0.1), you pass the already-broken float 0.1 into the constructor — and Decimal faithfully preserves every wrong digit of it. Always construct from a string (or an int), so the value is exact from the start.
# WRONG: the float 0.1 is already an approximation before Decimal sees it
print(Decimal(0.1)) # 0.1000000000000000055511151231257827021181583404541015625
# RIGHT: a string is interpreted exactly
print(Decimal('0.1')) # 0.1
Decimal(0.1) isn't an error — it's occasionally even what you want (to see exactly what a float holds). But for money and any decimal you typed or read as text, the string form is the only safe one.
Precision and context¶
Decimal arithmetic happens within a context that sets the precision — the number of significant digits kept, 28 by default. So division that doesn't terminate is rounded to 28 significant figures, not stored forever.
from decimal import Decimal, getcontext
print(getcontext().prec) # 28 — significant digits by default
print(Decimal(1) / Decimal(3)) # 0.3333333333333333333333333333 (28 threes)
getcontext().prec = 6 # narrow the context
print(Decimal(1) / Decimal(3)) # 0.333333
getcontext().prec = 28 # restore the default for later cells
Note that precision is significant digits, not decimal places. To pin a value to a fixed number of decimal places — like pence — you use quantize.
Rounding to the penny with quantize¶
quantize rounds a Decimal to the same number of decimal places as a template you give it (Decimal('0.01') for two places). It also lets you choose the rounding mode — and this is where you fix the banker's-rounding surprise from the last notebook. For money, people usually want ROUND_HALF_UP.
from decimal import Decimal, ROUND_HALF_UP, ROUND_HALF_EVEN
price = Decimal('0.125')
print(price.quantize(Decimal('0.01'))) # 0.12 (default: half to even)
print(price.quantize(Decimal('0.01'), rounding=ROUND_HALF_EVEN)) # 0.12 — 2 is even
print(price.quantize(Decimal('0.01'), rounding=ROUND_HALF_UP)) # 0.13 — the 'expected' rule
The full money pattern — parse, compute, then quantize once at the end — is laid out in the handle money with Decimal recipe.
Don't mix Decimal and float¶
You can add two Decimals, or a Decimal and an int, but mixing a Decimal with a float raises TypeError — deliberately, because combining an exact value with an approximate one defeats the purpose. Keep a calculation entirely in Decimal from input to output.
from decimal import Decimal
print(Decimal('1.50') + 1) # 2.50 — int is fine
try:
Decimal('1.50') + 0.1 # float is not
except TypeError as exc:
print('TypeError:', exc)
Fraction: exact rational numbers¶
Where Decimal is exact in base 10, Fraction is exact for any ratio of integers — it stores a numerator and denominator and keeps them reduced. So 1/3 is held precisely as 1/3, and thirds add up the way they should.
from fractions import Fraction
print(Fraction(1, 3) + Fraction(1, 6)) # 1/2 — exact, and auto-reduced
print(Fraction(1, 3) + Fraction(1, 3) + Fraction(1, 3)) # 1
print(Fraction(2, 4)) # 1/2 — reduced on construction
The same string-versus-float rule applies: Fraction('0.1') is exactly 1/10, while Fraction(0.1) captures the messy binary float. limit_denominator is the escape hatch — it finds the simplest fraction close to a float, which is great for recovering the "intended" value.
from fractions import Fraction
print(Fraction('0.1')) # 1/10 — exact from a string
print(Fraction(0.1)) # 3602879701896397/36028797018963968
print(Fraction(0.1).limit_denominator(1000)) # 1/10 — recovered
print(float(Fraction(1, 3))) # 0.3333333333333333 — convert out when needed
Which exact type, when?¶
Decimal— money and anything specified in decimal places with defined rounding. It thinks in tenths and hundredths, like an accountant.Fraction— exact ratios and rational arithmetic where thirds, sevenths, and the like must stay exact: probabilities, exact slopes, symbolic-ish maths.float— everything measured or continuous, where speed matters and a 16th-digit error doesn't.
Both exact types are slower than float and most arithmetic doesn't need them — but when correctness to the penny or the exact ratio matters, they're the difference between right and almost right. The choosing a numeric type essay turns this into a decision you can make quickly.
Recap¶
Decimalis exact in base 10 —Decimal('0.1') + Decimal('0.2') == Decimal('0.3').- Always build
Decimal/Fractionfrom strings, not floats, or you import the float's error. - Precision is significant digits (28 by default);
quantizepins decimal places and sets the rounding mode (ROUND_HALF_UPfor money). - Don't mix
Decimalandfloat— it raisesTypeErroron purpose. Fractionkeeps exact ratios, auto-reduced;limit_denominatorrecovers tidy values from floats.
Next: Maths, statistics, and random — the standard-library toolkit for actually computing things.