Python Closure
In these days of high inflation and high interest rates, I am sharing if you a simple Python Closure to calculate compound interest. According to Wikipedia, "a closure, also lexical closure or function closure, is a technique for implementing lexically scoped name binding in a language with first-class functions". Closure is an elegant style that takes the global namespace of your code clean. It behaves like a class/object instantiation, but in a much simpler way. I will use the class/object language to refer to the closure itself and its elements always in quotes, even if I know that it is not syntactically correct, just for the sake of ease of understanding.
Compound Interest
Our closure will calculate the compound interest for an initial value (principal) during a period of time and a fixed interest rate that you can set, based on the market conditions.
To create a closure the following criterias have to be met:
- Outer function and an inner function (nested functions).
- The inner function refers to some variable from the enclosing function.
- The outer function returns the inner function.
Find the criterias in the code below.
The closure behaves like a simple class, where you "instantiate" an "object" which points to the inner function returned by the closure.
You can "instantiate" any "objects" as you want, and the "objects" will hold the "instance variables".
Our closure has a parameter that receives the principal invested value as an argument. We "instantiate" two "objects" with different principal values.
Now we call the "instances" with the inner function arguments and each "instance" will remember the enclosing function principal value and calculate the values.
For this first version of the closure, the inner function receives arguments for interest rate and the invested period in years.
def compound_interest(principal):
def calculate(interest_rate=None, years_invested=None):
if not interest_rate or not years_invested:
raise ValueError("Missing arguments")
interest_rate /= 1200
months_invested = years_invested * 12
total = principal * (1 + interest_rate) ** months_invested
return total
return calculate
compound_interest_10k = compound_interest(10000)
print(compound_interest_10k(interest_rate=6, years_invested=10))
print(compound_interest_10k(interest_rate=5, years_invested=10))
compound_interest_50k = compound_interest(50000)
print(compound_interest_50k(interest_rate=6, years_invested=10))
print(compound_interest_50k(interest_rate=5, years_invested=10))
Output: ------ 18193.9673403229 16470.0949769028 90969.8367016145 82350.474884514
From here on, we will only add one more feature to calculate the compound interest, and we will also enrich the code with some Python best practices, and tests.
Add monthly deposits
Now the inner function of the closure will accept regular deposits and increse the total value.
The enclosing function is the same, only the inner function receives a new argument for the monthly deposits parameter, and sums it up to the principal value.
We are also using function type hints and enforcing the named parameters with the use of the * character before the keyword arguments.
def compound_interest(principal):
def calculate(*, monthly_deposit: float=None, interest_rate: float=None, years_invested: float=None) -> str:
if not interest_rate or not years_invested:
raise ValueError("Missing arguments")
interest_rate /= 1200
months_invested = years_invested * 12
total = principal * (1 + interest_rate) ** months_invested
if monthly_deposit:
total += monthly_deposit * ((((1 + interest_rate) ** months_invested) -1) / interest_rate)
return round(total, 2)
return calculate
compound_interest_10k = compound_interest(10000)
print(compound_interest_10k(monthly_deposit=1000, interest_rate=6, years_invested=10))
print(compound_interest_10k(monthly_deposit=500, interest_rate=5, years_invested=10))
compound_interest_50k = compound_interest(50000)
print(compound_interest_50k(monthly_deposit=1000, interest_rate=6, years_invested=10))
print(compound_interest_50k(monthly_deposit=500, interest_rate=5, years_invested=10))
Output: ------ 182073.31 94111.23 254849.18 159991.61
First class function
To take advantage of the Python first class function, we will add a feature to generate an output report using
a function callback.
We create the function "output_format_cb", which is passed as one of the default kwargs of the inner function of our closure.
This callback function can be overriden, or you can create another function to generate report as you need.
from typing import Callable
def output_format_cb(principal: float, total_monthly_deposit: float, interest_rate: float,
years_invested: float,total: float) -> str:
report = """
Pricipal: ${:0,.2f}
Total Deposits: ${:0,.2f}
Interest Rate: {}%
Years Invested: {} Years
Total: ${:0,.2f}
"""
report = report.format(principal, total_monthly_deposit, interest_rate, years_invested, total)
return report
def compound_interest(principal: float) -> Callable[[float, float, float, float], str]:
def calculate(*, monthly_deposit: float=None,
interest_rate: float=None,
years_invested: float=None,
format: Callable[[float, float, float, float, float], str]=output_format_cb) -> str:
if not interest_rate or not years_invested:
raise ValueError("Missing arguments")
interest_rate /= 1200
months_invested = years_invested * 12
total = principal * (1 + interest_rate) ** months_invested
if monthly_deposit:
total += monthly_deposit * ((((1 + interest_rate) ** months_invested) -1) / interest_rate)
return format(principal, monthly_deposit * months_invested, interest_rate * 1200, years_invested, round(total, 2))
return calculate
compound_interest_10k = compound_interest(10000)
print(compound_interest_10k(monthly_deposit=1000, interest_rate=6, years_invested=10))
print(compound_interest_10k(monthly_deposit=500, interest_rate=5, years_invested=10))
compound_interest_50k = compound_interest(50000)
print(compound_interest_50k(monthly_deposit=1000, interest_rate=6, years_invested=10))
print(compound_interest_50k(monthly_deposit=500, interest_rate=5, years_invested=10))
Output: ------ Pricipal: $10,000.00 Total Deposits: $120,000.00 Interest Rate: 6.0% Years Invested: 10 Years Total: $182,073.31 Pricipal: $10,000.00 Total Deposits: $60,000.00 Interest Rate: 5.0% Years Invested: 10 Years Total: $94,111.23 Pricipal: $50,000.00 Total Deposits: $120,000.00 Interest Rate: 6.0% Years Invested: 10 Years Total: $254,849.18 Pricipal: $50,000.00 Total Deposits: $60,000.00 Interest Rate: 5.0% Years Invested: 10 Years Total: $159,991.61
Add some tests
Some simple tests for the calculate function, before output formating.
def compound_interest(principal):
def calculate(*, monthly_deposit: float=None, interest_rate: float=None, years_invested: float=None) -> str:
if not interest_rate or not years_invested:
raise ValueError("Missing arguments")
interest_rate /= 1200
months_invested = years_invested * 12
total = principal * (1 + interest_rate) ** months_invested
if monthly_deposit:
total += monthly_deposit * ((((1 + interest_rate) ** months_invested) -1) / interest_rate)
return round(total, 2)
return calculate
import unittest
class TestCompoundInterest(unittest.TestCase):
def setUp(self) -> None:
self.compound_interest_10k = compound_interest(10000)
self.compound_interest_50k = compound_interest(50000)
def test_compound_interest_10k(self):
self.assertEqual(self.compound_interest_10k(monthly_deposit=1000, interest_rate=6, years_invested=10),
182073.31)
self.assertEqual(self.compound_interest_10k(monthly_deposit=500, interest_rate=5, years_invested=10),
94111.23)
def test_compound_interest_50k(self):
self.assertEqual(self.compound_interest_50k(monthly_deposit=1000, interest_rate=6, years_invested=10),
254849.18)
self.assertEqual(self.compound_interest_50k(monthly_deposit=500, interest_rate=5, years_invested=10),
159991.61)
if __name__ == '__main__':
unittest.main()
Output: ------ .. ---------------------------------------------------------------------- Ran 2 tests in 0.000s OK