Loan Amortization Schedule Builder
Compute fixed monthly payments and a full amortization table with Decimal rounding, then total the interest paid.
Code
from dataclasses import dataclass
from decimal import ROUND_HALF_UP, Decimal
CENT = Decimal("0.01")
def quantize(value: Decimal) -> Decimal:
return value.quantize(CENT, rounding=ROUND_HALF_UP)
@dataclass(frozen=True)
class Payment:
period: int
amount: Decimal
interest: Decimal
principal: Decimal
balance: Decimal
def monthly_payment(principal: Decimal, annual_rate: Decimal, months: int) -> Decimal:
if months <= 0:
raise ValueError("months must be positive")
rate = annual_rate / Decimal(12)
if rate == 0:
return quantize(principal / months)
growth = (Decimal(1) + rate) ** months
return quantize(principal * rate * growth / (growth - 1))
def amortization_schedule(
principal: Decimal,
annual_rate: Decimal,
months: int,
) -> list[Payment]:
rate = annual_rate / Decimal(12)
payment = monthly_payment(principal, annual_rate, months)
balance = principal
schedule: list[Payment] = []
for period in range(1, months + 1):
interest = quantize(balance * rate)
principal_part = payment - interest
if period == months:
principal_part = balance
payment = principal_part + interest
balance = quantize(balance - principal_part)
schedule.append(Payment(period, payment, interest, principal_part, balance))
return schedule
def total_interest(schedule: list[Payment]) -> Decimal:
return quantize(sum((row.interest for row in schedule), Decimal(0)))
def main() -> None:
schedule = amortization_schedule(Decimal("25000"), Decimal("0.065"), 24)
print(f"payment: {schedule[0].amount}")
for row in schedule[:3]:
print(row)
print("final balance:", schedule[-1].balance)
print("total interest:", total_interest(schedule))
if __name__ == "__main__":
main()Report a problem with this exampleOpens a pre-filled issue on GitHub.