# Q: Paying Debt Off a Year, MIT edX 6.001 U2/PS2/P1

Write a program to calculate the credit card balance after one year if a person only pays the minimum monthly payment required by the credit card company each month.

The following variables contain values as described below:

balance - the outstanding balance on the credit card

annualInterestRate - annual interest rate as a decimal

monthlyPaymentRate - minimum monthly payment rate as a decimal

For each month, calculate statements on the monthly payment and remaining balance. At the end of 12 months, print out the remaining balance. Be sure to print out no more than two decimal digits of accuracy - so print

Remaining balance: 813.41

Remaining balance: 813.4141998135

So your program only prints out one thing: the remaining balance at the end of the year in the format:

Remaining balance: 4784.0

A summary of the required math is found below:

\begin{align*} \text{Monthly interest rate} &= \frac{\text{Annual interest rate}}{12} \\ \text{Minimum monthly payment} &= \text{Minimum monthly payment rate} \times \text{Previous balance} \\ \text{Monthly unpaid balance} &= \text{Previous balance} \\ &- \text{Minimum monthly payment} \\ \text{Updated balance each month} &= \text{Monthly unpaid balance} \\ &+ (\text{Monthly interest rate} \times \text{Monthly unpaid balance}) \end{align*}

# Code

def balances(initial_balance, annual_interest_rate, minimum_monthly_payment_rate):
balance = initial_balance
monthly_interest_rate = annual_interest_rate / 12

for month in range(13):
minimum_monthly_payment = balance * minimum_monthly_payment_rate

monthly_unpaid_balance = balance - minimum_monthly_payment

yield {'month': month,
'minimum_monthly_payment': minimum_monthly_payment,
'balance': balance}

balance = monthly_unpaid_balance + (monthly_unpaid_balance * monthly_interest_rate)

def main():
*bs, remaining_balance = balances(balance, annualInterestRate, monthlyPaymentRate)
print('Remaining Balance: {}'.format(round(remaining_balance['balance'], 2)))

# Testing data
balance = 484
annualInterestRate = .2
monthlyPaymentRate = .04

## ifname_main does not work in grader
if __name__ == '__main__':
main()


• Could use some PEP484 type hints
• range(13) is awkward - there's no point in yielding the initial balance, so just yield adjusted balances and do it 12 times
• No point in yielding the month, payment and balance; just yield the balance. Even if you needed to yield all three, a dictionary is not a great way to do it. Tuples are standard; or a dataclass is better-structured.
• No need to call format or round. Just use .2f. Note that this contravenes the specification's Remaining balance: 4784.0 but that's dumb; this is a money quantity so the precision should be fixed.
• Your balances is fine as a generator, but why limit it to 12 iterations? The more generic and useful implementation simply iterates forever. Use islice to pull 12 values, ignoring all but the last.

## Suggested (iterative)

from itertools import islice
from typing import Iterable

def balances(
initial_balance: float,
annual_interest_rate: float,
minimum_monthly_payment_rate: float,
) -> Iterable[float]:
balance = initial_balance
monthly_interest_rate = annual_interest_rate / 12

while True:
minimum_monthly_payment = balance * minimum_monthly_payment_rate
monthly_unpaid_balance = balance - minimum_monthly_payment
balance = monthly_unpaid_balance * (1 + monthly_interest_rate)
yield balance

def main():
for balance in islice(
balances(
initial_balance=484.00,
annual_interest_rate=0.20,
minimum_monthly_payment_rate=0.04,
),
12,
):
pass

print(f'Remaining Balance: {balance:.2f}')

if __name__ == '__main__':
main()


Simplifying the inner loop would replace it with

balance *= (1 - minimum_monthly_payment_rate) * (1 + monthly_interest_rate)


which is equivalent.

## Suggested (compound)

Even better is to get rid of the loop entirely, and just use a power:

def compound(
initial_balance: float,
annual_interest_rate: float,
minimum_monthly_payment_rate: float,
months: int,
) -> float:
monthly_interest_rate = annual_interest_rate / 12

return initial_balance * (
(1 - minimum_monthly_payment_rate) * (1 + monthly_interest_rate)
) ** months

def main():
balance = compound(
initial_balance=484.00,
annual_interest_rate=0.20,
minimum_monthly_payment_rate=0.04,
months=12,
)

print(f'Remaining Balance: {balance:.2f}')

if __name__ == '__main__':
main()

• Wouldn't it be useless to yield negative balances? Maybe make it while balance >= 0 Jun 22, 2021 at 15:35
• Possible. Add that if you want. There are some (non-credit-card) account types that would benefit from being able to represent both credit and debit states. Jun 22, 2021 at 15:56
• That said, do a little math and you'll find that the balance never goes to or below zero for a finite number of iterations. Jun 22, 2021 at 16:58

The balances function is a bit long. The contents of the loop can be another function. This function shall be responsible to satisfy this requirement:

For each month, calculate statements on the monthly payment and remaining balance

Instead of yield-ing a structure with month, minimum_monthly_payment, and balance for each iteration of the loop, you can return at the end of the loop. Actually, month and minimum_monthly_payment is not used at all, so you may only return the remaining balance at the end of 12 months.

Since the balances function now returns only a single number, you can get rid of the complex code at main. Just get that number and print it.

After those changes, rename the balances method to something more meaningful.