Loan Amortization Schedule Builder

Compute fixed monthly payments and a full amortization table with Decimal rounding, then total the interest paid.

PythonData Integrity

Code

snippet
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()