Watch the video above, then read on for the detail. This post goes deeper into how Python handles numbers so you can write financial calculations that work without hidden errors.
Why Python's Number Handling Matters in Finance
You write a cash position report. It looks clean. It balances. Then six months later, a colleague spots a rounding error in a liquidity calculation that's been running unnoticed. The code looked correct. Python ran it without complaint. But somewhere deep, floats were doing what floats do: losing precision in ways that are hard to see coming.
Understanding how Python actually stores and manipulates numbers is not academic. It is the difference between code that works and code that works until it doesn't. As a finance professional writing Python for treasury, risk, or reporting, you need to know when to trust Python's arithmetic, when to be careful, and when to reach for a different tool entirely.
Integers and Floats: How Python Stores Them
Python has two primary numeric types you'll use: integers and floats.
Integers are exact. Python stores them as whole numbers with no loss of precision, however large or small. You can add, multiply, or divide integers and Python will honour the mathematics exactly.
Floats are approximate. Python stores them in a format called IEEE 754 binary floating point. This is fast and works for most purposes, but it cannot represent all decimal numbers exactly. It stores a number as a sign bit, a mantissa (the significant digits), and an exponent. This works beautifully for numbers like 0.5 or 0.25, which have exact binary representations. It fails for numbers like 0.1 or 0.3, which do not.
Here is what that looks like in practice:
# Integers: exact
a = 10
b = 3
print(a + b) # 13, exact
# Floats: approximate
x = 0.1
y = 0.2
print(x + y) # 0.30000000000000004, not 0.3
Why does this matter? In cash position reporting, you reconcile to the penny. A rounding error of a millionth of a basis point can accumulate across thousands of transactions. In accrual accounting, interest calculations over time must sum to the agreed amount. If you use floats throughout, tiny errors compound.
Arithmetic Operations and Type Coercion
When you combine an integer and a float in a single operation, Python automatically converts the result to a float. This is called type coercion.
rate = 0.05 # float, the annual interest rate
days = 30 # integer
accrued_interest = rate * days / 365
print(type(accrued_interest)) # <class 'float'>
print(accrued_interest) # 0.004109589041095890
This is usually sensible. A rate is a float. A day count is an integer. The result must be a float. But it also means the result carries the float approximation problem.
Type coercion happens silently. Python does not warn you. It just converts and continues. This is fine for display and most intermediate calculations. It becomes a problem when you need to store the result or compare it directly.
# Type coercion in action
principal = 1000000 # integer, pounds
rate = 0.0325 # float, 3.25%
days_held = 45 # integer
interest = principal * rate * days_held / 365
print(f"Interest accrued: {interest}")
# Interest accrued: 4013.698630136986
The interest is correct to several decimal places, but those trailing digits after 4013.69 are noise introduced by float representation. If you round it, you handle it. If you pass it downstream without rounding, you are passing approximation along the chain.
Type coercion is automatic and silent. It is not an error; it is a feature. But in finance, automatic means easy to miss.
The Float Precision Problem: A Real Example
The classic example is 0.1 + 0.2:
print(0.1 + 0.2) # 0.30000000000000004
print(0.1 + 0.2 == 0.3) # False
# This looks like a bug. It is not. It is how binary floats work.
Why does this happen?
0.1 in binary is a repeating fraction, like 1/3 in decimal. Python stores it in a finite number of bits, so it truncates. When you add two truncated approximations, the error compounds slightly. The result is not exactly 0.3.
This is not a flaw in Python. Any language using IEEE 754 (which covers all major programming languages) has the same behaviour. The issue is the binary representation of decimal fractions, not the implementation.
This is why you never compare floats directly with ==. Instead, use an epsilon (tolerance):
def floats_equal(a, b, tolerance=1e-9):
return abs(a - b) < tolerance
print(floats_equal(0.1 + 0.2, 0.3)) # True
In a real scenario, imagine you are calculating the accrual on a floating rate note. You need to sum daily interest over a month and reconcile it to the final valuation.
# Floating rate bond, daily accrual
base_rate = 0.05
spread = 0.0125
rate = base_rate + spread
daily_accrual = []
for day in range(30):
daily_rate = rate / 365
daily_accrual.append(daily_rate)
total_accrued = sum(daily_accrual)
print(f"Total accrued: {total_accrued}")
print(f"Expected: {rate * 30 / 365}")
print(f"Difference: {abs(total_accrued - (rate * 30 / 365))}")
# Difference: 5.551115123125783e-16
# Not large, but present.
With millions in notional and hundreds of transactions per day, tiny errors in the wrong direction add up to real audit questions.
When to Use the Decimal Module
When you need exact decimal arithmetic, use Python's Decimal module. It stores numbers in decimal form, not binary, so it has no representation error for decimal values.
Here is the problem with floats first:
# Floats: approximate
principal = 100000.00
rate = 0.0325
months = 6
interest_float = principal * rate * months / 12
print(f"Interest (float): {interest_float}")
# Interest (float): 1625.0000000000002
Now with Decimals:
Members get exclusive videos, early access and community perks on the channel.
from decimal import Decimal, ROUND_HALF_UP
# Decimals: exact
principal = Decimal('100000.00')
annual_rate = Decimal('0.0325')
months = 6
interest = principal * annual_rate * Decimal(months) / Decimal(12)
interest_rounded = interest.quantize(Decimal('0.01'), rounding=ROUND_HALF_UP)
print(f"Interest (Decimal): £{interest_rounded}")
# Interest (Decimal): £1625.00
Note that you pass strings to Decimal, not floats. If you pass a float, it first converts the float (which is already approximate) and then stores it as a Decimal. You get precision in representation, but not in the underlying value.
# Right way
x = Decimal('0.1') + Decimal('0.2')
print(x) # 0.3
# Wrong way
y = Decimal(0.1) + Decimal(0.2)
print(y) # 0.3000000000000000166533453693773481063544750213623046875
When should you use Decimal in finance code?
- Money calculations: any time you are summing currency amounts or calculating interest that must round to a specific decimal place.
- Regulatory reporting: ILAAP, ICAAP, and PRA110 reporting often require calculations rounded to specific decimal places and auditable arithmetic.
- Accrual and interest: wherever the calculation must be exact or reconcile to a known total.
The quantize method rounds to a specific number of decimal places using the rounding mode you specify. ROUND_HALF_UP is standard in UK banking and aligns with regulatory expectations.
Division: Integer, Float, and Floor
Python has three division operators, and it is easy to mix them up.
/ (true division) always returns a float, even if both operands are integers:
print(10 / 3) # 3.3333333333333335
print(10 / 2) # 5.0
// (floor division) always returns an integer (or Decimal if operands are Decimal). It rounds down to the nearest lower integer:
print(10 // 3) # 3
print(-10 // 3) # -4 (not -3, because -4 is lower)
% (modulo) returns the remainder after floor division:
print(10 % 3) # 1
print(-10 % 3) # 2
In treasury and risk work, you use these operators in different contexts:
- True division (
/) for rates, yields, and interpolation where precision matters. - Floor division (
//) for day counts, position sizes in units (you cannot hold a fractional bond if the minimum is 1). - Modulo (
%) to detect if a number divides evenly, or to break a position into tranches.
Example: breaking a EUR cash position into 1 million note amounts:
position = 37500000 # EUR, 37.5 million
note_size = 1000000 # 1 million notes
full_notes = position // note_size
remainder = position % note_size
print(f"Full 1M notes: {full_notes}")
print(f"Remaining: €{remainder:,}")
# Full 1M notes: 37
# Remaining: €500,000
Working with Variables in Complex Formulas
Real finance calculations combine multiple variables and operations. The key is to structure them so they are readable and the rounding is explicit.
Net Present Value (NPV)
from decimal import Decimal
def calculate_npv(cash_flows, discount_rate):
"""
Calculate NPV of a series of cash flows.
cash_flows: list of tuples (year, amount)
discount_rate: decimal rate per annum
"""
npv = Decimal('0')
for year, amount in cash_flows:
discount_factor = Decimal('1') / ((Decimal('1') + discount_rate) ** year)
# Convert amount to string first to avoid float approximation
pv = Decimal(str(amount)) * discount_factor
npv += pv
return npv
flows = [
(0, -100000),
(1, 30000),
(2, 35000),
(3, 40000)
]
rate = Decimal('0.05')
npv_result = calculate_npv(flows, rate)
print(f"NPV: £{npv_result.quantize(Decimal('0.01'))}")
# NPV: £-9819.96
Funding Transfer Pricing (FTP) Spread
from decimal import Decimal, ROUND_HALF_UP
balance = Decimal('500000')
internal_ftp_rate = Decimal('0.035')
external_funding_cost = Decimal('0.042')
days_held = 45
year_days = 365
ftp_income = (balance * internal_ftp_rate * Decimal(days_held)) / Decimal(year_days)
funding_cost = (balance * external_funding_cost * Decimal(days_held)) / Decimal(year_days)
ftp_spread = ftp_income - funding_cost
ftp_spread_pence = ftp_spread.quantize(Decimal('0.01'), rounding=ROUND_HALF_UP)
print(f"FTP Spread (45 days): £{ftp_spread_pence}")
# FTP Spread (45 days): £120.55
Notice that we convert all inputs to Decimal, do the calculation, and only round at the end for reporting. This is the defensive pattern.
Defensive Patterns for Production Code
When you write code that will run in production and affect reporting or decision making, use these patterns:
1. Be explicit about type.
# Good
amount = Decimal('50000.00')
rate = Decimal('0.0325')
# Risky
amount = 50000.00 # looks good, but it is a float
rate = 0.0325 # already approximate
2. Separate input, calculation, and output.
# Input: always convert to the type you need
input_rate = float('3.25') / 100
rate = Decimal(str(input_rate)).quantize(Decimal('0.0001'))
# Calculation: work in Decimal
result = principal * rate
# Output: round explicitly
report_value = result.quantize(Decimal('0.01'))
3. Round only once, at the end, using an explicit rounding mode.
from decimal import Decimal, ROUND_HALF_UP
# Do not do this
result = interest.quantize(Decimal('0.01'))
final = result * 2 # rounding errors can persist
# Do this
result = interest * 2
final = result.quantize(Decimal('0.01'), rounding=ROUND_HALF_UP)
4. Test edge cases and reconcile backwards.
def accrue_interest(principal, rate, days, rounding_places=2):
from decimal import Decimal, ROUND_HALF_UP
p = Decimal(str(principal))
r = Decimal(str(rate))
d = Decimal(str(days))
accrual = (p * r * d) / Decimal('365')
return accrual.quantize(Decimal(10) ** -rounding_places, rounding=ROUND_HALF_UP)
# Test
principal = 1000000
rate = 0.05
days = 30
daily_accrual = accrue_interest(principal, rate, 1)
monthly_accrual = accrue_interest(principal, rate, days)
# Reconcile: 30 daily accruals should equal the monthly accrual
reconstructed = daily_accrual * 30
print(f"Daily * 30: {reconstructed}")
print(f"Monthly: {monthly_accrual}")
print(f"Match: {reconstructed == monthly_accrual}")
This last pattern is crucial. Before you deploy, calculate the same number two ways and verify they match. It catches logic errors and precision issues before they hit production.
Put This Into Practice
You now understand how Python represents numbers, how type coercion works, and when precision matters enough to use Decimal. You have seen the patterns that experienced practitioners use to write financial code that works without hidden errors.
Start with one calculation you own. Rewrite it defensively. Test it. Then you know.
The Industry Portal Academy offers structured Python for finance courses if you want to go deeper.
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.
