Skip to content

Instantly share code, notes, and snippets.

@jt-hill
Created November 28, 2025 19:18
Show Gist options
  • Select an option

  • Save jt-hill/839e1a6c51d28cbcb8fc69e6143193eb to your computer and use it in GitHub Desktop.

Select an option

Save jt-hill/839e1a6c51d28cbcb8fc69e6143193eb to your computer and use it in GitHub Desktop.
Decimal vs float precisoin and speed tests
"""
Decimal vs Float64: Does it matter for financial calculations?
"""
from decimal import Decimal
import time
# TEST 1: Single 30-year mortgage
print("TEST 1: Single 30-year mortgage (360 payments)")
print("-" * 50)
principal = 300_000
rate = 0.06
periods = 360
# Decimal version
p_d = Decimal("300000")
r_d = Decimal("0.06") / 12
pmt_d = p_d * (r_d * (1 + r_d) ** 360) / ((1 + r_d) ** 360 - 1)
bal_d = p_d
for _ in range(360):
bal_d = bal_d * (1 + r_d) - pmt_d
# Float version
r_f = 0.06 / 12
pmt_f = principal * (r_f * (1 + r_f) ** 360) / ((1 + r_f) ** 360 - 1)
bal_f = float(principal)
for _ in range(360):
bal_f = bal_f * (1 + r_f) - pmt_f
print(f"Final balance (Decimal): ${float(bal_d):.20f}")
print(f"Final balance (Float64): ${bal_f:.20f}")
print(f"Difference: ${abs(float(bal_d) - bal_f):.20f}")
print(f"Match when rounded to cents: {round(float(bal_d), 2) == round(bal_f, 2)}")
# TEST 2: Total interest paid across 1000 loan portfolio
print("\nTEST 2: Total interest paid (1000 loans, full amortization)")
print("-" * 50)
import random
random.seed(42)
total_interest_d = Decimal("0")
total_interest_f = 0.0
for _ in range(1000):
principal = random.randint(100_000, 500_000)
rate = random.uniform(0.04, 0.08)
# Decimal
p_d = Decimal(str(principal))
r_d = Decimal(str(rate)) / 12
pmt_d = p_d * (r_d * (1 + r_d) ** 360) / ((1 + r_d) ** 360 - 1)
bal_d = p_d
for _ in range(360):
interest_d = bal_d * r_d
total_interest_d += interest_d
bal_d = bal_d + interest_d - pmt_d
# Float
r_f = rate / 12
pmt_f = principal * (r_f * (1 + r_f) ** 360) / ((1 + r_f) ** 360 - 1)
bal_f = float(principal)
for _ in range(360):
interest_f = bal_f * r_f
total_interest_f += interest_f
bal_f = bal_f + interest_f - pmt_f
print(f"Decimal total: ${float(total_interest_d):,.20f}")
print(f"Float64 total: ${total_interest_f:,.20f}")
print(f"Difference: ${abs(float(total_interest_d) - total_interest_f):20f}")
print(
f"Match to cent: {round(float(total_interest_d), 2) == round(total_interest_f, 2)}"
)
# TEST 3: Daily interest accrual over 30 years
print("\nTEST 3: Daily interest accrual (30 years = 10,950 days)")
print("-" * 50)
principal_d = Decimal("300000.00")
daily_rate_d = Decimal("0.06") / 365
principal_f = 300000.0
daily_rate_f = 0.06 / 365
balance_d = principal_d
balance_f = principal_f
for _ in range(10_950): # 30 years of daily compounding
balance_d = balance_d * (1 + daily_rate_d)
balance_f = balance_f * (1 + daily_rate_f)
print(f"Decimal final: ${float(balance_d):,.2f}")
print(f"Float64 final: ${balance_f:,.2f}")
print(f"Difference: ${abs(float(balance_d) - balance_f):.6f}")
print(f"Match to cent: {round(float(balance_d), 2) == round(balance_f, 2)}")
# TEST 4: Sum of small amounts
print("\nTEST 4: Sum of 1,000,000 × $0.01")
print("-" * 50)
d_sum = sum(Decimal("0.01") for _ in range(1_000_000))
f_sum = sum(0.01 for _ in range(1_000_000))
print(f"Decimal: ${float(d_sum):,.2f}")
print(f"Float64: ${f_sum:,.6f}")
print(f"Difference: ${abs(float(d_sum) - f_sum):.10f}")
print(f"Match to cent: {round(float(d_sum), 2) == round(f_sum, 2)}")
# TEST 5: Performance
print("\nTEST 5: Performance (1000 loan amortizations)")
print("-" * 50)
def amortize_decimal():
p = Decimal("300000")
r = Decimal("0.06") / 12
pmt = p * (r * (1 + r) ** 360) / ((1 + r) ** 360 - 1)
bal = p
for _ in range(360):
bal = bal * (1 + r) - pmt
return bal
def amortize_float():
p = 300000.0
r = 0.06 / 12
pmt = p * (r * (1 + r) ** 360) / ((1 + r) ** 360 - 1)
bal = p
for _ in range(360):
bal = bal * (1 + r) - pmt
return bal
# Decimal
start = time.perf_counter()
for _ in range(1000):
amortize_decimal()
decimal_time = time.perf_counter() - start
# Float
start = time.perf_counter()
for _ in range(1000):
amortize_float()
float_time = time.perf_counter() - start
print(f"Decimal: {decimal_time * 1000:.0f}ms")
print(f"Float64: {float_time * 1000:.0f}ms")
print(f"Speedup: {decimal_time / float_time:.1f}x")
# TEST 6: Pandas aggregation
print("\nTEST 6: Pandas aggregation (100k rows)")
print("-" * 50)
import pandas as pd
import numpy as np
data = [Decimal(str(i * 0.01)) for i in range(100_000)]
df_obj = pd.DataFrame({"amt": data}) # object dtype
df_f64 = pd.DataFrame({"amt": np.array([float(d) for d in data])})
start = time.perf_counter()
for _ in range(100):
df_obj["amt"].sum()
obj_time = time.perf_counter() - start
start = time.perf_counter()
for _ in range(100):
df_f64["amt"].sum()
f64_time = time.perf_counter() - start
print(f"Decimal (object dtype): {obj_time * 1000:.0f}ms")
print(f"Float64 (native dtype): {f64_time * 1000:.0f}ms")
print(f"Speedup: {obj_time / f64_time:.0f}x")
# TEST 7: scipy.optimize
print("\nTEST 7: scipy.optimize compatibility")
print("-" * 50)
from scipy.optimize import newton
cfs = [-100000, 30000, 35000, 40000, 45000]
# Float works
irr = newton(lambda r: sum(cf / (1 + r) ** i for i, cf in enumerate(cfs)), 0.1)
print(f"Float64 IRR: {irr:.4%} ✓")
# Decimal fails
cfs_d = [Decimal(str(cf)) for cf in cfs]
try:
newton(
lambda r: sum(cf / (1 + Decimal(str(r))) ** i for i, cf in enumerate(cfs_d)),
Decimal("0.1"),
)
print("Decimal IRR: worked (unexpected)")
except TypeError as e:
print(f"Decimal IRR: TypeError ✗")
print(f" '{str(e)[:60]}...'")
# TEST 8: Silent Decimal-to-float conversion (pyxirr and numpy-financial)
print("\nTEST 8: Silent Decimal-to-float conversions")
print("-" * 50)
# PyXIRR
from pyxirr import xirr
from datetime import date
dates = [date(2020, 1, 1), date(2021, 1, 1), date(2022, 1, 1)]
amounts_decimal = [Decimal("-1000.00"), Decimal("500.00"), Decimal("600.00")]
result = xirr(dates, amounts_decimal)
print("pyxirr.xirr():")
print(f" Input type: {type(amounts_decimal[0]).__name__}")
print(f" Output type: {type(result).__name__}")
# numpy-financial
import numpy_financial as npf
amounts_decimal = [Decimal("-1000.00"), Decimal("500.00"), Decimal("600.00")]
result = npf.irr(amounts_decimal)
print("numpy_financial.irr():")
print(f" Input type: {type(amounts_decimal[0]).__name__}")
print(f" Output type: {type(result).__name__}")
print()
print("Both accept Decimal but silently convert to float64 internally.")
# TEST 9: Decimal constructor footgun
print("\nTEST 9: Decimal constructor footgun")
print("-" * 50)
correct = Decimal("100.10")
wrong = Decimal(100.10)
print(f'Decimal("100.10") = {correct}')
print(f"Decimal(100.10) = {wrong}")
print()
print("Miss the quotes and you've embedded the float error into your 'exact' Decimal.")
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment