Decimal

Floating-point arithmetic can't represent most decimal fractions exactly — the decimal module trades some speed for exact, controllable base-10 arithmetic where correctness matters most.

float in Python is a binary floating-point number, and most decimal fractions (like 0.1) cannot be represented exactly in binary -- this is fine for scientific computation but dangerous for money and other exact decimal quantities. The decimal module provides Decimal, an arbitrary-precision, base-10 numeric type with explicit control over precision and rounding, built exactly for the cases where float's imprecision is unacceptable.

The Float Imprecision Problem

Computers store float values in binary floating-point (IEEE 754 double precision). Many decimal fractions that look simple in base 10 -- 0.1, 0.2, 0.3 -- have no exact finite binary representation, the same way 1/3 has no exact finite decimal representation. The stored value is the closest representable binary approximation, which is usually invisible but becomes visible under arithmetic.

This is a property of binary floating-point representation itself, not a Python bug -- the same behavior appears in essentially every language that uses IEEE 754 doubles.

For scientific and engineering computation, this tiny imprecision is usually harmless and float is the right, fast choice. For money and other values where exact decimal representation matters -- and where small errors compound over many transactions -- it's a real problem.

print(0.1 + 0.2)          # 0.30000000000000004 -- not exactly 0.3
print(0.1 + 0.2 == 0.3)   # False

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

What Decimal Provides

decimal.Decimal represents numbers in base 10 exactly (up to a configurable precision), so values that are exact in decimal notation -- like 0.1 or 19.99 -- are stored and manipulated exactly, with no binary-conversion error.

from decimal import Decimal

a = Decimal("0.1")
b = Decimal("0.2")
print(a + b)              # 0.3 -- exact
print(a + b == Decimal("0.3"))  # True

Creating Decimals Correctly

Always construct a Decimal from a string (or an int), never from a float literal -- constructing from a float bakes in whatever binary-approximation error that float already has, defeating the entire purpose.

Decimal(0.1) is exact -- but exact about the wrong number, because float 0.1 was never exactly 0.1 to begin with. Decimal("0.1") is exact about the number you actually meant.

Constructing from an int (Decimal(19)) is always safe, since ints have no binary-fraction imprecision -- the danger is specifically constructing from a non-integer float.

from decimal import Decimal

good = Decimal("0.1")
bad = Decimal(0.1)  # inherits float's binary approximation error

print(good)   # 0.1
print(bad)    # 0.1000000000000000055511151231257827021181583404541015625

Context: Precision and Rounding Rules

Every Decimal operation is performed under a *context* that controls precision (how many significant digits are kept) and the rounding rule applied when a result doesn't fit exactly. decimal.getcontext() returns the thread-local default context; decimal.localcontext() gives a temporary, scoped context for a with block.

from decimal import Decimal, getcontext, localcontext

print(getcontext().prec)   # default precision, 28 significant digits

with localcontext() as ctx:
    ctx.prec = 4
    result = Decimal("1") / Decimal("3")
    print(result)           # 0.3333 -- rounded to 4 significant digits

# outside the with block, the default context (prec=28) applies again
print(Decimal("1") / Decimal("3"))

Rounding Modes

The decimal module supports multiple explicit rounding modes (ROUND_HALF_UP, ROUND_HALF_EVEN -- the default, also called "banker's rounding", ROUND_DOWN, ROUND_CEILING, ROUND_FLOOR, and others), set via the context or passed to .quantize() for rounding to a specific number of decimal places..quantize() rounds to match the exponent of its argument -- Decimal("0.01") means "round to two decimal places".

from decimal import Decimal, ROUND_HALF_UP

price = Decimal("19.995")
rounded = price.quantize(Decimal("0.01"), rounding=ROUND_HALF_UP)
print(rounded)   # 20.00

# ROUND_HALF_EVEN (the default) rounds .5 cases to the nearest even digit,
# which reduces cumulative bias over many roundings:
print(Decimal("2.5").quantize(Decimal("1")))  # 2 (rounds to even)
print(Decimal("3.5").quantize(Decimal("1")))  # 4 (rounds to even)

Arithmetic with Decimal

Decimal supports the usual arithmetic operators (+, -, *, /, `, %) and mixes freely with int, but not** directly with float -- mixing raises TypeError`, which is a deliberate guard against silently reintroducing binary-float imprecision into exact decimal math.

from decimal import Decimal

subtotal = Decimal("49.99") + Decimal("12.50")
tax = subtotal * Decimal("0.08")
total = (subtotal + tax).quantize(Decimal("0.01"))
print(subtotal, tax, total)

try:
    Decimal("1.0") + 0.5   # float -- raises TypeError
except TypeError as exc:
    print("error:", exc)

print(Decimal("1.0") + 1)  # int mixes fine

Financial Calculations: A Worked Case

Money is the canonical use case for Decimal: currency amounts are inherently decimal (dollars and cents), users expect exact and predictable rounding, and small binary-float errors compounding over thousands of transactions can produce real accounting discrepancies.

sum() with an explicit Decimal("0") start value avoids ever mixing in the int 0 in a way that would change the result's type unexpectedly (it wouldn't here, but the explicit start is good practice and required in some stricter contexts).

from decimal import Decimal, ROUND_HALF_UP

TWO_PLACES = Decimal("0.01")

def apply_tax(amount: Decimal, rate: Decimal) -> Decimal:
    return (amount * rate).quantize(TWO_PLACES, rounding=ROUND_HALF_UP)

items = [Decimal("9.99"), Decimal("14.50"), Decimal("3.25")]
subtotal = sum(items, Decimal("0"))
tax = apply_tax(subtotal, Decimal("0.0825"))
total = (subtotal + tax).quantize(TWO_PLACES, rounding=ROUND_HALF_UP)

print(f"Subtotal: {subtotal}")
print(f"Tax:      {tax}")
print(f"Total:    {total}")

Decimal Versus Float: When to Use Which

Use float for scientific/engineering computation, physics simulations, statistics, and anywhere performance matters and tiny relative errors are acceptable and expected -- float operations are implemented in hardware and are much faster than Decimal's software arithmetic. Use Decimal for money, exact accounting, and any domain where users expect base-10 digits to behave exactly the way they look on paper, and where the cost of a wrong penny is unacceptable.

Use float when:                    Use Decimal when:
- scientific/engineering math       - currency and financial totals
- large-scale numeric performance   - exact decimal digit behavior matters
- small relative error is fine      - regulatory/accounting exactness needed
- interoperating with numpy/etc.    - user-facing decimal amounts (prices)

Practical Usage

Typical patterns: read monetary values from user input or a database as strings and construct Decimal directly from those strings; keep a fixed quantization step (.quantize(Decimal("0.01"))) applied consistently at the boundary where a value becomes "currency" for display or storage; avoid intermediate rounding in the middle of a calculation chain, quantizing only at the final, presented result unless the domain (e.g. accounting rules) specifically requires intermediate rounding.

Edge Cases

Decimal("NaN") and Decimal("Infinity") are valid special values, mirroring float's nan/inf, and follow IEEE-854-style rules for how they propagate through arithmetic.

Division that doesn't terminate in the current precision (like 1/3) is rounded to the context's precision rather than raising -- results are always exact up to the configured number of significant digits, not infinitely exact.

Comparing Decimal to float with ==/< etc. is allowed and compares by value (unlike arithmetic, which raises), but can still surface float's imprecision on the float side of the comparison -- prefer comparing Decimal to Decimal.

float(some_decimal) converts to a float and reintroduces binary approximation error -- only do this deliberately (e.g. for a library that requires float input), never as a routine step in money-handling code.

Best Practices

Always construct from strings or ints, never float literals. Set precision/rounding deliberately via context rather than relying on defaults for financial code where the rounding rule matters legally or contractually. Use .quantize() at defined boundaries (e.g. when displaying or persisting a final total), not on every intermediate step. Never mix Decimal and float in arithmetic -- convert deliberately, in one direction, and document why.

Complete Example

Naive division would leave a rounding remainder unaccounted for; this pattern distributes leftover cents so the shares sum exactly to the original total -- a real concern only Decimal makes tractable to reason about precisely.

from decimal import Decimal, ROUND_HALF_UP, localcontext

CENTS = Decimal("0.01")

def split_bill(total: Decimal, num_people: int) -> list[Decimal]:
    """Split a bill into num_people shares that sum exactly to total."""
    with localcontext() as ctx:
        ctx.prec = 10
        base_share = (total / num_people).quantize(CENTS, rounding=ROUND_HALF_UP)

    shares = [base_share] * num_people
    remainder = total - base_share * num_people
    # Distribute leftover cents one at a time so shares still sum exactly
    cents_left = int((remainder / CENTS).to_integral_value())
    for i in range(abs(cents_left)):
        shares[i] += CENTS if cents_left > 0 else -CENTS
    return shares

bill = Decimal("100.00")
shares = split_bill(bill, 3)
print(shares, sum(shares) == bill)   # shares sum exactly to 100.00

Quick Reference

Decimal("0.1")               construct from a string -- always
Decimal(5)                    construct from an int -- always safe
Decimal(0.1)                  DON'T -- inherits float's binary error
getcontext().prec             default significant-digit precision (28)
localcontext()                scoped context for a 'with' block
.quantize(Decimal("0.01"))    round to a fixed number of decimal places
ROUND_HALF_EVEN (default)     banker's rounding, reduces cumulative bias
ROUND_HALF_UP                 conventional "round .5 up" behavior
Decimal + float               TypeError -- convert deliberately, not by accident

Key Takeaways

float uses binary floating-point, so most decimal fractions have no exact representation -- fine for science/engineering, risky for money.

Decimal represents base-10 numbers exactly up to a configurable precision, controlled by a context object.

Always construct Decimal from strings or ints, never from float literals, or you inherit float's imprecision anyway.

.quantize() with an explicit rounding mode gives predictable, controllable rounding for a fixed number of decimal places.

Decimal and float cannot be mixed directly in arithmetic (TypeError) -- this is a deliberate safeguard, not a limitation to work around casually.

Use Decimal for money and exact accounting; use float for scientific/engineering computation where speed matters and tiny relative error is acceptable.

price = Decimal(19.99) price = Decimal("19.99") Constructing from a float literal bakes in that float's binary approximation error; constructing from the string gives the exact intended value.

total = Decimal("10.00") + 0.5 total = Decimal("10.00") + Decimal("0.5")

Decimal and float cannot be mixed in arithmetic (raises TypeError); convert the float to a Decimal deliberately via a string first.

rounded = round(Decimal("2.345"), 2) # relies on binary float rounding semantics under the hood in some contexts rounded = Decimal("2.345").quantize(Decimal("0.01"), rounding=ROUND_HALF_UP)

Using .quantize() with an explicit rounding mode is unambiguous and lets you choose the exact rounding rule instead of depending on round()'s default behavior.

Why does 0.1 + 0.2 not equal 0.3 when using Python floats?

Because float is binary floating-point, and 0.1, 0.2, and 0.3 have no exact finite binary representation; each is stored as the closest representable approximation, and the tiny errors don't cancel out perfectly during addition.

Why must Decimal be constructed from a string rather than a float literal for exact results?

Decimal(some_float) captures whatever binary approximation error that float already has baked in (since the float was created before Decimal ever saw it); Decimal("0.1") starts from the exact decimal text and has no such inherited error.

What does a Decimal context control, and what are its two most important settings?

It controls how Decimal arithmetic operations are performed: precision (how many significant digits results are kept to) and the rounding mode applied when an exact result doesn't fit that precision.

What does .quantize() do, and how do you specify the number of decimal places to round to?

It rounds a Decimal to match the exponent of another Decimal passed as its argument -- e.g. .quantize(Decimal("0.01")) rounds to two decimal places -- optionally with an explicit rounding mode.

Why does Python raise TypeError when you try to add a Decimal and a float directly?

It's a deliberate safeguard: silently converting one to the other would reintroduce (or hide) binary-float imprecision into what's meant to be exact decimal arithmetic, so Python forces an explicit, visible conversion instead.

When should you prefer float over Decimal, and why?

For scientific/engineering computation, large-scale numeric work, or anywhere performance matters and small relative errors are acceptable -- float arithmetic is hardware-accelerated and much faster than Decimal's arbitrary-precision software arithmetic.

What is ROUND_HALF_EVEN ('banker's rounding'), and why is it the default?

It rounds a value exactly halfway between two options to whichever neighbor is even, rather than always rounding up; over many roundings this reduces systematic upward bias compared to always-round-up rules, which is why it's the module's default.

Write a function total_price(unit_price, quantity, tax_rate) that computes a tax-inclusive total using Decimal, rounded to two decimal places with ROUND_HALF_UP, given unit_price and tax_rate as strings.

from decimal import Decimal, ROUND_HALF_UP def total_price(unit_price: str, quantity: int, tax_rate: str) -> Decimal: price = Decimal(unit_price) * quantity tax = price * Decimal(tax_rate) return (price + tax).quantize(Decimal("0.01"), rounding=ROUND_HALF_UP) print(total_price("9.99", 3, "0.0825"))

Decimal('32.44') -- 9.99 * 3 = 29.97, plus 8.25% tax (2.472525), summed and rounded half-up to two decimal places gives 32.44.

What does this print, and why doesn't it print an infinitely repeating value?

0.3333333333333333333333333333 (28 threes, the default context precision) -- Decimal division that doesn't terminate exactly is rounded to the context's configured precision (28 significant digits by default) rather than computed to infinite precision.

Write a function safe_divide(a, b) that divides two Decimal values and returns None instead of raising when b is zero, using exception handling.

from decimal import Decimal, DivisionByZero def safe_divide(a: Decimal, b: Decimal) -> Decimal | None: try: return a / b except DivisionByZero: return None print(safe_divide(Decimal("10"), Decimal("2"))) print(safe_divide(Decimal("10"), Decimal("0")))

Decimal('5'), then None -- Decimal division by zero raises decimal.DivisionByZero (a subclass of ArithmeticError, distinct from float's behavior of producing inf), which is caught and handled here.

from decimal import Decimal
print(Decimal("10") / Decimal("3"))