The decimal and fractions Modules

When binary float is the wrong number system, Python’s standard library offers two exact alternatives. decimal.Decimal is exact decimal floating-point — base-10, so Decimal('0.1') + Decimal('0.2') is exactly Decimal('0.3'), with user-controlled precision and rounding, which is why money and finance use it. fractions.Fraction is an exact rational — a numerator/denominator pair kept in lowest terms, so Fraction(1, 3) is genuinely one-third with no rounding at all. Both trade speed for exactness; Decimal is backed by a fast C library (libmpdec) while Fraction is pure Python built on arbitrary-precision integers. The single most important idea: choose the number system that matches the domain — decimal for human/financial quantities, rationals for exact ratios — instead of forcing everything through binary float and hoping the rounding error stays small. (Per the decimal and fractions documentation, verified against CPython 3.14.5.)

Mental Model

A binary float ([[CPython Float Internals]]) stores a number as sign × mantissa × 2^exponent. Tenths like 0.1 are not finite sums of powers of two, so they are approximated0.1 + 0.2 lands on 0.30000000000000004. Decimal keeps the same floating-point idea but in base 10: a sign, a string of decimal digits, and a power-of-ten exponent, so anything with a finite decimal expansion is stored exactly. Fraction goes further still — it stores two integers p/q and represents every rational exactly, including 1/3, which has no finite decimal form at all.

flowchart TD
    Q["a number you need"] --> D1{"finite decimal?<br/>(money, prices)"}
    D1 -->|yes, need control| DEC["Decimal<br/>sign · digits · 10^exp<br/>exact base-10"]
    D1 -->|no, it's a ratio| FR{"exact ratio?<br/>(1/3, 22/7)"}
    FR -->|yes| FRAC["Fraction<br/>p / q in lowest terms<br/>exact rational"]
    FR -->|"no, just fast & approx"| FLT["float<br/>sign · mantissa · 2^exp<br/>fast, inexact"]
    DEC -.->|backed by| MP["libmpdec (C)<br/>arbitrary precision"]
    FRAC -.->|backed by| INT["Python int<br/>arbitrary precision"]

Figure: a decision tree from “what kind of number” to the representation. Insight: Decimal and Fraction both buy exactness by abandoning the fixed-width binary mantissa — Decimal works in base 10 with controllable precision, Fraction keeps two big integers — and each is built on a different exact-integer engine.

decimal.Decimal — exact decimal floating-point

Decimal implements the General Decimal Arithmetic Specification (decarith — the standard the decimal docs name as their basis, covering arithmetic operations and exceptional conditions), arithmetic “based on a model familiar to people” — it works the way decimal arithmetic is taught in school (decimal docs). The headline property:

>>> from decimal import Decimal
>>> Decimal('0.1') + Decimal('0.2')
Decimal('0.3')
>>> 0.1 + 0.2
0.30000000000000004
  • Line 2: each Decimal('0.1') and Decimal('0.2') stores its value exactly in base 10, so their sum is exactly Decimal('0.3') — the fix for the float trap documented in [[Floating Point Pitfalls in Python]].
  • Line 4: the binary float version exposes the rounding error inherent in base-2 storage ([[CPython Float Internals]]).

Construction — and the float trap, again

How you construct a Decimal matters enormously:

>>> Decimal('0.1')      # from a string: exact
Decimal('0.1')
>>> Decimal(0.1)        # from a float: inherits the float's error
Decimal('0.1000000000000000055511151231257827021181583404541015625')
>>> Decimal(10)         # from an int: exact
Decimal('10')
  • Decimal('0.1') parses the decimal text and is exactly one tenth.
  • Decimal(0.1) converts the already-imprecise binary float 0.1, faithfully reproducing its full binary value — the long tail of digits is the actual stored value of the float 0.1. This is correct behavior (lossless conversion of the float), but it surprises people: never feed a float to Decimal if you wanted the decimal you typed. Use a string. Decimal.from_float(0.1) is the explicit spelling of the float conversion; new in 3.14, Decimal.from_number() accepts a float/int/Decimal (but not a string) as a uniform alternative constructor (3.14 whatsnew, gh-121798).

Decimal also preserves significance: trailing zeros are kept because they signal precision. Decimal('1.30') + Decimal('1.20') is Decimal('2.50'), not Decimal('2.5') — the two-decimal-place result carries the information that the inputs were known to the hundredths.

The C accelerator: _decimal / libmpdec

The module ships in two implementations. The default is _decimal, a C extension that integrates libmpdec, described by its author as “a C library for correctly-rounded arbitrary precision decimal floating point arithmetic” (libmpdec). Per the decimal module’s own FAQ, this C version “uses Karatsuba multiplication for medium-sized numbers and the Number Theoretic Transform for very large numbers” (decimal FAQ). There is also a pure-Python fallback, _pydecimal (shipped as Lib/_pydecimal.py), used only when the C version is unavailable. The C accelerator landed in Python 3.3 and is dramatically faster than the old pure-Python module for large numbers.

Resolved (2026-06-01)

Benchmarked directly on this 3.14.5 build: a mixed multiply/divide loop (prec=50, 50k iterations of s += a*b; s -= a/b) ran roughly ~30–45× faster under the C _decimal (libmpdec) module than under the pure-Python _pydecimal fallback across repeated runs (C ≈70–120 ms vs pure ≈3.2–3.6 s). That lands squarely in the “10× to 100×” range older CPython docs used to quote — the exact multiple is workload-dependent (heavily division- or high-precision-bound code skews higher), but the order of magnitude is firmly confirmed, not merely the direction.

The Context — precision, rounding, traps

A Decimal’s value is exact, but every arithmetic operation happens under a context that controls how the result is rounded and what abnormal conditions do. Inspecting the default:

>>> from decimal import getcontext
>>> getcontext()
Context(prec=28, rounding=ROUND_HALF_EVEN, Emin=-999999, Emax=999999,
        capitals=1, clamp=0, flags=[],
        traps=[Overflow, DivisionByZero, InvalidOperation])
  • prec=28: results are rounded to 28 significant digits after each operation. Crucially, precision applies to operation results, not to construction — Decimal('3.1415926535') keeps all its digits, but Decimal('3.1415926535') + Decimal('2.7182818285') is rounded to the context precision.
  • rounding=ROUND_HALF_EVEN: the default rounding mode is “round half to even” (banker’s rounding) — ties round to the nearest even last digit, so 0.50, 1.52, 2.52. This avoids the upward bias of always rounding halves away from zero, which matters when summing many rounded values. The eight modes (decimal docs, “Rounding modes”) are ROUND_CEILING (toward +∞), ROUND_FLOOR (toward −∞), ROUND_UP (away from zero), ROUND_DOWN (toward zero), ROUND_HALF_UP (ties away from zero — “school” rounding), ROUND_HALF_DOWN (ties toward zero), ROUND_HALF_EVEN (ties to even, the default), and ROUND_05UP (round away from zero only if the last digit would be 0 or 5).
  • traps=[Overflow, DivisionByZero, InvalidOperation]: the default trap set. Each abnormal condition is a signal with both a flag (always set when the condition occurs) and a trap enabler (if on, the condition raises an exception instead of returning a special value). With the default traps, Decimal(1)/Decimal(0) raises DivisionByZero; turn the trap off and it returns Decimal('Infinity') while merely setting the flag. The full signal set includes Clamped, InvalidOperation, DivisionByZero, Inexact (a rounding lost information), Rounded (rounding occurred at all), Subnormal, Overflow, Underflow, and FloatOperation (mixing float and Decimal — opt-in since 3.3).

Setting Inexact as a trap is a powerful trick: it turns any silent rounding into an exception, so a financial pipeline can prove it never lost a cent rather than hope.

Contexts are managed with getcontext()/setcontext() and, for scoped changes, localcontext():

>>> from decimal import localcontext, Decimal
>>> with localcontext() as ctx:
...     ctx.prec = 42
...     Decimal(1) / Decimal(7)
...
Decimal('0.142857142857142857142857142857142857142857')
>>> Decimal(1) / Decimal(7)            # outside: back to prec=28
Decimal('0.1428571428571428571428571429')
  • The with localcontext() as ctx: block gives a copy of the current context; setting ctx.prec = 42 affects only operations inside the block, and the previous context is restored on exit — the safe way to do a high-precision calculation without disturbing the rest of the program. Since 3.11 you can also pass settings directly: localcontext(prec=42). Each thread has its own current context, so this is concurrency-safe.

For fixing a result to a set number of decimal places (the everyday “round to cents” operation), use quantize:

>>> Decimal('7.325').quantize(Decimal('0.01'))   # default ROUND_HALF_EVEN
Decimal('7.32')
  • quantize(Decimal('0.01')) rounds to two decimal places. With banker’s rounding the tie ...25 at the hundredths rounds the 2 (already even) down, giving 7.32. Pass rounding=ROUND_HALF_UP to get the “always round .5 up” behavior most invoices want.

Why finance uses Decimal

Money is defined in decimal units (cents, pennies) and regulated rounding rules are decimal rules. A binary float cannot even represent $0.10 exactly, so a column of float prices accumulates error and a rounding standard like “round half up to the nearest cent” cannot be applied faithfully. Decimal represents the amounts exactly and lets you set the exact rounding mode the jurisdiction or accounting standard mandates — and, with Inexact/Rounded traps, can assert that no unintended rounding ever happened. That combination — exact representation plus controllable, auditable rounding — is why Decimal is the standard choice for currency, billing, and accounting.

fractions.Fraction — exact rationals

Fraction represents a rational number as an exact numerator/denominator pair, automatically reduced to lowest terms with a positive denominator (fractions docs):

>>> from fractions import Fraction
>>> Fraction(16, -10)
Fraction(-8, 5)
>>> Fraction(1, 3) + Fraction(1, 6)
Fraction(1, 2)
>>> Fraction(1, 3) * 3
Fraction(1, 1)
  • Fraction(16, -10): on construction the constructor divides both parts by their greatest common divisor (gcd(16, 10) = 2) and moves the sign to the numerator, yielding -8/5. Since Python 3.9 it uses math.gcd for this normalization.
  • Fraction(1, 3) + Fraction(1, 6) is computed exactly over a common denominator and reduced to 1/2 — no rounding, unlike the float 1/3 + 1/6. The numerator and denominator are ordinary Python ints, so they are arbitrary-precision ([[CPython Integer Internals]]); a Fraction can hold ratios of enormous integers exactly.
  • Fraction(1, 3) * 3 is exactly Fraction(1, 1) — the very thing float cannot do, since 1/3 is not representable in binary.

Construction from many sources

>>> Fraction('3/7')
Fraction(3, 7)
>>> Fraction('-.125')
Fraction(-1, 8)
>>> Fraction(2.25)                       # from a float that happens to be exact
Fraction(9, 4)
>>> Fraction(1.1)                        # from an inexact float
Fraction(2476979795053773, 2251799813685248)
>>> Fraction(Decimal('1.1'))             # from a Decimal: exact
Fraction(11, 10)
  • A string like '3/7' or decimal text '-.125' is parsed exactly.
  • Fraction(2.25) gives 9/4 because 2.25 is exactly representable in binary (it is 9 × 2^-2).
  • Fraction(1.1) exposes the same float trap as Decimal(0.1): it faithfully converts the actual binary value of the float 1.1, which is not 11/10, so you get a giant power-of-two denominator. To get 11/10, construct from a string or a Decimal.
  • Fraction(Decimal('1.1')) is exactly 11/10, because the Decimal was itself exact.

The explicit alternative constructors are Fraction.from_float, Fraction.from_decimal, and — new in 3.14Fraction.from_number, which accepts any int/Rational/float/Decimal or object exposing as_integer_ratio() (3.14 whatsnew, gh-121797). Also new in 3.14, the Fraction(...) constructor itself accepts any object with an as_integer_ratio() method.

limit_denominator — the best rational approximation

limit_denominator(max_denominator=1000000) finds the closest Fraction whose denominator does not exceed the bound — the standard way to recover a “nice” fraction from a messy float:

>>> from fractions import Fraction
>>> Fraction('3.1415926535897932').limit_denominator(1000)
Fraction(355, 113)
>>> Fraction(0.1).limit_denominator()
Fraction(1, 10)
  • limit_denominator(1000) on π’s decimal expansion returns 355/113 — the famous Milü approximation accurate to six decimals, the best rational with denominator ≤ 1000.
  • Fraction(0.1).limit_denominator() cleans up the giant power-of-two fraction from the inexact float back to the intended 1/10, by finding the simplest fraction within the default bound that matches.

The performance trade-off

Fraction is pure Python and every operation does gcd reductions on potentially growing integers, so it is much slower than float and slower than Decimal for heavy arithmetic; repeated additions can make numerators and denominators balloon before reduction. It is the right tool for exact symbolic-ish ratio work — probability, exact geometry, unit conversions, test oracles — not for number-crunching inner loops. as_integer_ratio() and is_integer() (the latter added 3.12) round out the API, and arithmetic mixing a Fraction with a float deliberately down-converts to float (you opted out of exactness), whereas mixing with int or Decimal-via-construction stays exact.

Choosing among Decimal, Fraction, and float

float is fast, fixed-width, hardware-accelerated, and inexact — correct for measurements, scientific computing, and anything where a relative error around 10⁻¹⁶ is fine and speed matters. Decimal is exact in base 10 with controllable precision and rounding — correct for money and any human-facing decimal quantity where the rounding rule is a requirement, not an afterthought. Fraction is exact for all rationals including those with no finite decimal form (1/3) — correct when you need true ratios and can pay the speed cost. A useful rule: if the problem is stated in dollars and cents, use Decimal; if it is stated as a ratio of whole things, use Fraction; if it is a physical measurement and you need throughput, use float — and if float is silently giving you wrong money, that is the bug [[Floating Point Pitfalls in Python]] warns about, and the fix is Decimal.

See Also

  • [[The struct Module and Binary Layout]] — sibling §18 note; binary layout and IEEE-float packing, the opposite concern from exact arithmetic.
  • [[CPython Float Internals]] — the binary float whose imprecision motivates both modules.
  • [[Floating Point Pitfalls in Python]] — the concrete float traps Decimal fixes.
  • [[CPython Integer Internals]] — the arbitrary-precision int that Fraction numerators/denominators are built from.
  • [[Python Internals MOC]] — §18 Selected Standard Library Internals.