Money, taxes, rates, measurements, and accounting rules require predictable results. The built-in float type is fast and appropriate for many scientific tasks, but it represents numbers in binary. Simple decimal fractions such as 0.1 therefore cannot always be stored exactly. The Python Decimal module provides decimal arithmetic with configurable precision, explicit rounding rules, and mechanisms for detecting inexact operations.
This guide explains correct construction, quantize(), contexts, rounding modes, traps, input validation, API serialization, and database storage. It complements our articles about Python floats, formatting numbers and currency, data types, UUIDs, and object copying.
Why float can surprise you
Binary floating point stores many decimal values as approximations:
print(0.1 + 0.2) # 0.30000000000000004
print(0.1 + 0.1 + 0.1 == 0.3) # FalseThis is not a Python-specific bug. The official floating-point tutorial explains that 0.1 has an infinite expansion in base 2, just as 1/3 has an infinite expansion in base 10. Hardware stores the closest representable binary fraction.
For simulations and graphics, the tiny error is usually acceptable. For accounting, prices, and strict decimal equality invariants, base-10 arithmetic is often a better model.
Create Decimal values from strings
Import Decimal and pass a string containing the intended value:
from decimal import Decimal
price = Decimal("19.90")
rate = Decimal("0.075")
total = price * (Decimal("1") + rate)
print(total)The string preserves exactly the digits supplied, including significant trailing zeros. This is the preferred construction for form data, textual JSON, configuration files, and database values.
Avoid constructing Decimal from float
Passing a float converts the binary approximation exactly:
from decimal import Decimal
print(Decimal(0.1))
# 0.100000000000000005551115123125782...This is useful when inspecting the real stored float, but it does not represent the user’s intended decimal. Prefer Decimal("0.1"). If another library returns a float and your deliberate policy is to use its displayed decimal form, convert through str() and document the choice:
value = Decimal(str(0.1))Integers and Decimal.from_number()
Integers convert exactly:
quantity = Decimal(3)
subtotal = Decimal("9.90") * quantityPython 3.14 adds Decimal.from_number(), which accepts int, float, or another Decimal, but not strings. A float is still converted to its complete binary-equivalent decimal value.
value = Decimal.from_number(314)
print(value) # 314Significance and trailing zeros
Decimal preserves trailing zeros to communicate significance:
from decimal import Decimal
print(Decimal("1.30") + Decimal("1.20")) # 2.50
print(Decimal("1.30") * Decimal("1.20")) # 1.5600The numeric value 2.50 equals 2.5, but the representation can communicate two decimal places. This matters in financial reports and measured values.
Rounding with quantize()
quantize() rounds a number to the exponent of another Decimal. To produce two decimal places:
from decimal import Decimal, ROUND_HALF_UP
CENT = Decimal("0.01")
value = Decimal("7.325")
result = value.quantize(CENT, rounding=ROUND_HALF_UP)
print(result) # 7.33Use a constant for the scale. Rounding must happen where the business rule requires it: per item, per invoice line, per tax component, or only on the final total. Different rounding points can produce different totals.
ROUND_HALF_EVEN versus ROUND_HALF_UP
The default context uses ROUND_HALF_EVEN, often called banker’s rounding. Exact ties go to the final even digit, reducing accumulated bias.
from decimal import Decimal, ROUND_HALF_EVEN, ROUND_HALF_UP
value = Decimal("2.5")
print(value.quantize(Decimal("1"), rounding=ROUND_HALF_EVEN)) # 2
print(value.quantize(Decimal("1"), rounding=ROUND_HALF_UP)) # 3Many commercial rules require ROUND_HALF_UP; others require truncation, ceiling, or floor. Follow the legal or domain specification rather than choosing the most familiar name.
Other rounding modes
The module also provides ROUND_DOWN, ROUND_UP, ROUND_FLOOR, ROUND_CEILING, ROUND_HALF_DOWN, and ROUND_05UP. The difference between down and floor becomes visible with negative numbers: down moves toward zero, while floor moves toward negative infinity.
Test positive values, negative values, and exact ties. A suite that covers only 1.235 can still hide a bug for -1.235.
The decimal context
The official Decimal documentation organizes the module around numbers, contexts, and signals. A context defines precision, rounding, exponent limits, flags, and traps.
from decimal import Decimal, getcontext
ctx = getcontext()
print(ctx.prec) # 28 by default
print(ctx.rounding)
ctx.prec = 10
print(Decimal(1) / Decimal(7))Precision means significant digits in arithmetic, not decimal places in the final display. Construction from a string preserves all input digits; the context applies during calculations.
Use localcontext() for temporary changes
Changing the active context inside reusable library code can surprise callers. Use localcontext() to isolate a calculation:
from decimal import Decimal, localcontext
with localcontext(prec=50) as ctx:
result = Decimal(1) / Decimal(7)
print(result)
# previous context restoredThis is ideal for a high-precision stage that should not affect the rest of the application. Python 3.11 and later support context attributes as keyword arguments.
Signals and sticky flags
Operations can signal conditions such as Inexact, Rounded, DivisionByZero, InvalidOperation, Overflow, and Underflow. Flags remain set until cleared.
from decimal import Decimal, getcontext
ctx = getcontext()
ctx.clear_flags()
Decimal(1) / Decimal(7)
print(ctx.flags)Flags allow an application to audit a batch without interrupting it. Clear them before the monitored calculation, process the batch, and inspect the conditions afterward.
Traps turn signals into exceptions
An enabled trap makes a signal raise an exception. This is useful when a workflow cannot accept silent rounding.
from decimal import Decimal, Inexact, localcontext
with localcontext() as ctx:
ctx.traps[Inexact] = True
try:
Decimal("1") / Decimal("3")
except Inexact:
print("Inexact result is not allowed")Traps are valuable in financial validation, imports, and tests. Catch specific exceptions and report a meaningful error instead of silently accepting inconsistent data.
Detecting accidental float use
The FloatOperation signal can prevent floats from entering a decimal workflow accidentally.
from decimal import Decimal, FloatOperation, localcontext
with localcontext() as ctx:
ctx.traps[FloatOperation] = True
Decimal(3.14) # raises FloatOperationThis protection is useful in financial modules because it forces the team to make each conversion policy explicit.
Price, discount, and tax example
from decimal import Decimal, ROUND_HALF_UP
CENT = Decimal("0.01")
price = Decimal("149.90")
quantity = Decimal("3")
discount = Decimal("0.10")
tax = Decimal("0.075")
subtotal = price * quantity
after_discount = subtotal * (Decimal("1") - discount)
total = after_discount * (Decimal("1") + tax)
total = total.quantize(CENT, rounding=ROUND_HALF_UP)
print(total)A real application must define whether discount and tax apply per item or on the aggregate. Decimal implements the rule; it does not decide the accounting rule.
Percentages and rates
Convert 7.5% to Decimal("0.075"). Do not divide a float by 100 before conversion. For localized text, normalize separators with an explicit parser and reject ambiguous formats.
def percentage(text: str) -> Decimal:
value = Decimal(text.replace(",", "."))
return value / Decimal("100")This simple helper should not accept thousands separators or currency symbols without locale-aware validation. A value such as “1,234.56” can mean different things in different regions.
Comparisons and mixed types
Adding Decimal and float raises TypeError, which avoids silent mixing:
Decimal("1.2") + 1.2 # TypeErrorIntegers work naturally. Comparisons with floats are supported, but they reflect the binary approximation. In a decimal domain, normalize both operands before comparing.
NaN, Infinity, and signed zero
Decimal supports special values:
from decimal import Decimal
values = [
Decimal("NaN"),
Decimal("Infinity"),
Decimal("-Infinity"),
Decimal("-0"),
]Do not allow these values into prices or balances without an explicit policy. Validate with is_finite(), is_nan(), and is_infinite().
JSON serialization
The standard json module does not serialize Decimal automatically. Converting to float reintroduces approximation. Financial APIs often send a string:
record = {
"total": str(Decimal("19.90")),
"currency": "USD",
}Document the API contract so consumers parse the field as a decimal value. Framework-specific encoders can also preserve Decimal semantics.
Database storage
Use DECIMAL or NUMERIC columns with defined precision and scale. Do not store money in a binary FLOAT column. ORMs usually map numeric columns to Decimal.
Validate limits before insertion. A NUMERIC(10,2) column cannot hold arbitrary magnitudes, and a database may round or reject values depending on its configuration.
Performance trade-offs
Decimal is usually slower than float because it provides additional precision and policy controls. Do not replace every float automatically. Use float for graphics, physics, and tolerant numerical work; use Decimal for exact decimal values and regulated rounding.
Measure large pipelines. Storing cents as integers can be simpler when currency and scale are fixed. Interest, exchange rates, and fractional quantities often benefit from Decimal.
Testing decimal calculations
Tests should cover rounding ties, negative numbers, limits, malformed input, and operation order.
from decimal import Decimal, ROUND_HALF_UP
assert Decimal("2.675").quantize(
Decimal("0.01"), rounding=ROUND_HALF_UP
) == Decimal("2.68")Compare Decimal with Decimal, not float. Also test invariants such as installment totals, tax sums, and reconciliation with persisted values.
Common mistakes
- Calling
Decimal(0.1)while expecting exact 0.1. - Rounding only for display when the accounting rule requires real rounding.
- Changing the active context inside reusable code.
- Mixing Decimal and float.
- Converting to float before serialization.
- Choosing a rounding mode without checking domain rules.
- Ignoring inexact and rounded flags.
- Using Decimal everywhere without measuring performance.
Best practices
- Create values from strings or integers.
- Centralize scale constants and rounding modes.
- Use
localcontext()for temporary precision. - Enable
FloatOperationin critical modules. - Require finite values.
- Store in DECIMAL/NUMERIC columns.
- Serialize as strings when precision must survive.
- Test financial rules with authoritative examples.
Conclusion
The Python Decimal module offers controls that binary floating point was not designed to provide: exact decimal representation, significant trailing zeros, configurable precision, explicit rounding modes, and auditable signals.
Correct use starts at input. Construct from strings, define scale with quantize(), isolate contexts, and block accidental float mixing. When business rules, APIs, and database columns share the same decimal contract, money, rates, and measurements become reproducible, testable, and far less likely to differ by a cent.







