Watch the video above, then read on for the detail and practitioner context. This post walks you through Python's Decimal module and shows you when and how to use it in your treasury and regulatory work.
Why Floats Break in Finance
Your code looks right, the logic is sound, and then your LCR haircut calculation lands 0.000001 short of the regulatory threshold. Or an interest accrual compounds over months and drifts by a basis point. These are not bugs in your code. They are the cost of how Python represents floating point numbers.
Floats store numbers in binary. Most decimal fractions cannot be represented exactly in binary, so they are stored as the closest approximation. 0.1 in decimal, for instance, becomes a repeating binary fraction. When you add floats together, those small approximation errors accumulate. In banking, precision is a requirement, not optional. A £10 million position accrued over a year could drift by £50 or more. Silent rounding errors can breach regulatory thresholds, break audit trails, or create reconciliation headaches.
Treasury analysts deal with money, rates, and regulatory haircuts. A position valuation error of £1 repeated across a thousand positions is real profit and loss impact. A LCR calculation that rounds down instead of according to the rulebook is a compliance issue. Floats will not tell you when they fail. That is why finance code needs a different tool.
What the Decimal Module Does
The Decimal module, part of Python's standard library, stores decimal numbers as an exact coefficient and exponent, not as a binary approximation. A Decimal value like 0.1 is stored as 1 and 1 respectively (1 × 10^-1), so it is represented precisely. Arithmetic with Decimals works the way you learned it at school, not the way binary floating point works.
Decimal also gives you explicit control over rounding. You set a precision context, and all Decimal arithmetic in that context respects it. No surprises, no hidden rounding modes. This matters in regulatory reporting where you need to justify every basis point.
The trade off is speed. Decimal arithmetic is slower than float arithmetic because it does not use the CPU's hardware floating point unit. In most treasury workflows, that cost is acceptable. In heavy numerical simulation or real time market data feeds, you may need to profile and decide. We will come back to that.
Creating and Using Decimals
From Strings, Not Floats
The first rule: create Decimals from strings, never from floats.
from decimal import Decimal
# Wrong: you inherit the float's imprecision
bad = Decimal(0.1)
print(bad) # 0.1000000000000000055511151231257827021...
# Right: the string is interpreted exactly
good = Decimal("0.1")
print(good) # 0.1
When you pass a float to Decimal, it takes the already imprecise binary representation and wraps it. You have not solved the problem, you have buried it.
When you pass a string, Decimal parses it and stores the exact decimal value. This is the only safe way to start.
Basic Arithmetic
Once you have Decimals, arithmetic works as you would expect:
from decimal import Decimal
principal = Decimal("1000.00")
rate = Decimal("0.045") # 4.5% annual
days = Decimal("90")
year_days = Decimal("365")
# Simple interest accrual
accrued_interest = principal * rate * (days / year_days)
print(accrued_interest) # 11.095890410958904109589041095890410959 (exact)
# Exact balance
balance = principal + accrued_interest
print(balance) # 1011.095890410958904109589041095890410959
Each operation is exact. There is no approximation, no silent rounding. If you need to know the result to 16 decimal places, you set the precision context and Decimal will round according to your rules.
Context and Precision
The precision context controls how many significant figures Decimal keeps and how it rounds. By default, Decimal precision is 28 significant figures. For most banking work, that is more than enough. But you can change it.
from decimal import Decimal, getcontext
# Check the current context
ctx = getcontext()
print(ctx.prec) # 28
# Set precision for the entire session
getcontext().prec = 10
narrow = Decimal("1") / Decimal("3")
print(narrow) # 0.3333333333 (10 significant figures)
# Reset for the next calculation
getcontext().prec = 28
wide = Decimal("1") / Decimal("3")
print(wide) # 0.3333333333333333333333333333
You can also create a local context for a single block of code:
from decimal import Decimal, localcontext
result_wide = Decimal("1") / Decimal("3")
print(result_wide) # Uses global precision (28)
with localcontext() as local_ctx:
local_ctx.prec = 4
result_narrow = Decimal("1") / Decimal("3")
print(result_narrow) # 0.3333 (4 figures only)
result_wide_again = Decimal("1") / Decimal("3")
print(result_wide_again) # Back to global (28)
In treasury systems, you often need different precision for different workflows. A funding cost pass through might round to 4 decimal places (basis points). A full accrual might keep 10 places. Set the context at the start of each calculation block.
Members get exclusive videos, early access and community perks on the channel.
Real Treasury and Regulatory Examples
Interest Accrual Calculation
A classic use case: daily interest accrual on a funding position. Tiny rounding errors compound into reconciliation problems.
from decimal import Decimal, getcontext
getcontext().prec = 10 # 10 significant figures
principal = Decimal("10000000.00") # 10 million
annual_rate = Decimal("0.035") # 3.5%
days_in_year = Decimal("365")
# Accrue for 30 days
daily_accrual = principal * annual_rate / days_in_year
total_accrual = daily_accrual * Decimal("30")
print(f"Daily: {daily_accrual}")
print(f"30 day accrual: {total_accrual}")
# Now check the balance
balance = principal + total_accrual
print(f"Balance: {balance}")
With floats, the accumulated rounding error would be visible. With Decimals, the result is exact to the precision you set. When you report it to the regulator or to the trading desk, you know the number is sound.
Position Valuation and Rounding
You value a position using a market rate (which comes from a data feed) and need to round to the nearest penny for reporting.
from decimal import Decimal, ROUND_HALF_UP
# Market data often comes as a float; convert it safely
market_rate = Decimal(str(1.2345)) # Always convert via string
position_qty = Decimal("5000")
position_value_exact = position_qty * market_rate
print(f"Exact value: {position_value_exact}")
# Round to 2 decimal places (pence) using ROUND_HALF_UP
position_value_rounded = position_value_exact.quantize(
Decimal("0.01"),
rounding=ROUND_HALF_UP
)
print(f"Rounded: {position_value_rounded}")
The quantize method sets the number of decimal places and applies a rounding rule. ROUND_HALF_UP is the standard banking convention (round 0.5 up). Other options exist: ROUND_DOWN, ROUND_CEILING, ROUND_FLOOR. Choose the one your rulebook requires.
LCR Haircut Precision
Liquidity coverage ratio calculations often involve haircuts to eligible collateral, applied with strict rounding rules. A 10% haircut on a collateral value must be exact.
from decimal import Decimal, ROUND_DOWN
collateral_value = Decimal("1000000.00")
haircut_rate = Decimal("0.10") # 10%
haircut_amount = (collateral_value * haircut_rate).quantize(
Decimal("0.01"),
rounding=ROUND_DOWN # Regulatory haircuts commonly round down for prudence
)
eligible_collateral = collateral_value - haircut_amount
print(f"Haircut: {haircut_amount}")
print(f"Eligible: {eligible_collateral}")
With floats, the haircut amount might round differently than your rulebook expects. With Decimals and explicit rounding, you control the outcome and can prove it to the auditor.
Regulatory calculations are not just numerically precise. They are procedurally precise. Decimal lets you document and enforce the rounding rule that the rulebook requires, not whatever float happens to do.
Mixing Decimals and Floats
If you mix Decimals and floats in an expression, Python will convert one or the other. The result is usually a Decimal, but precision is already compromised because the float came in imprecise.
from decimal import Decimal
d = Decimal("0.1")
f = 0.2 # A float
result = d + f
print(type(result)) # <class 'decimal.Decimal'>
print(result) # 0.3000000000000000166533453694... (imprecise because of the float)
The rule is simple: convert floats to Decimals from strings before any calculation.
from decimal import Decimal
d = Decimal("0.1")
f = Decimal("0.2") # Converted from float via string first (or received as a string from your data)
result = d + f
print(result) # 0.3 (exact)
In practice, this means: if your interest rates come from a market data feed, parse them as strings into Decimals before you use them. If a user enters a haircut percentage in a UI form, accept it as a string and convert to Decimal. This is not extra work. It is the price of correctness.
Performance Considerations
Decimal arithmetic is 5 to 20 times slower than float arithmetic, depending on the operation and the precision set. In most treasury workflows, that cost is absorbed. Accruing interest on a portfolio of thousands of positions, even with Decimals, runs in milliseconds. Regulatory reporting workflows are rarely time critical.
But if you are updating market data feeds in real time, or running a Monte Carlo simulation with millions of scenarios, or processing tick by tick market data, the Decimal overhead may matter. Profile your code. Use the timeit module:
import timeit
from decimal import Decimal
float_time = timeit.timeit(
lambda: 0.1 + 0.2 + 0.3,
number=1000000
)
print(f"Float: {float_time:.4f} seconds")
decimal_time = timeit.timeit(
lambda: Decimal("0.1") + Decimal("0.2") + Decimal("0.3"),
number=1000000
)
print(f"Decimal: {decimal_time:.4f} seconds")
If the performance gap matters in your workflow, use floats in the inner loop (if precision loss is acceptable there) and convert to Decimals only where it counts: for final valuations, regulatory calculations, and reporting. Hybrid approaches are valid.
When to Use Decimal (and When Not To)
Use Decimal for:
- Interest accrual and rate calculations
- Position valuations and profit and loss
- Regulatory reporting (LCR, NSFR, IRRBB)
- FTP and funding cost allocations
- Any figure that appears in a financial statement or regulatory return
- Reconciliation workflows where audit trail precision matters
Consider floats (or other numeric types) for:
- Market data streaming if speed is critical and you round to Decimal later
- Intermediate calculations in numerical models where final output is rounded to Decimal
- Large scale simulations where Decimal overhead would be prohibitive
In practice, for treasury work, Decimal is the default. Use floats only if you have measured that it matters.
Practical Takeaway
Start using Decimal for any financial calculation you write. Create Decimals from strings, never from floats. Set your precision context at the start of a calculation block. Use quantize with an explicit rounding rule (ROUND_HALF_UP, ROUND_DOWN, etc.) when you report a number. When you mix data sources, always convert floats to Decimals via strings before arithmetic.
The performance cost is real but small for most treasury workflows. The correctness gain is absolute. Your accruals will reconcile. Your regulatory calculations will be defensible. Your audit trail will be clean. That is worth the investment.
Members get exclusive videos, early access and community perks on the channel.

The Complete Python Course
Welcome to the most practical and beginner friendly Python Bootcamp Course on YouTube.
Take the courseGet the next one in your inbox
A weekly note across Finance & Treasury, Innovation & Automation and Career Development. No spam, unsubscribe any time.
Notes across finance and treasury, innovation and automation, and career development, written by practitioners who do the work.
