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

Popular posts from this blog

Atom - Jupyter / Hydrogen

Design Patterns

Robson Koji Moriya disambiguation name