Loops and Lists

for Loops and Lifetime Savings

Because Python is quick and easy (once you have a handle of it), it serves as a useful tool for playing out ideas. For example, suppose that we want to visualize a savings decision:

Once you begin contributing to a savings account, assume that you put away \(x\) dollars per year and expect an annual return of \(r\). How much money will you have saved in \(55\) years?

Begin by defining the variables you need

x = 5000
r = 0.06

Now, compound the returns you get over the next \(55\) years. Imagine that today is year 0. At the end of year 0, you’ll have saved \(x \times (1+r)\). Then, after another, you’re year zero savings grow by another \((1+r)\), and your year one savings (since you put away \(x\) more each year), also grows by \((1+r)\). The following table helps visualize the idea:

Time

Value of Money Saved at Year 0

Value of Money Saved at Year 1

Value of Money Saved at Year 2

Value of Money Saved at Year 3

0

\(x\)

.

.

.

1

\(x(1+r)\)

\(x\)

.

.

2

\(x(1+r)^2\)

\(x(1+r)\)

\(x\)

.

3

\(x(1+r)^3\)

\(x(1+r)^2\)

\(x(1+r)\)

\(x\)

Add up the values across each column to get the total savings at the beginning of a given year. This sum is reported in the following table:

Time Total Savings (beginning of year) Total Savings (algebraically re-arranged)
$0$ $x$ $x$
$1$ $x(1+r)+x$ $x(1+r)+x$
$2$ $x(1+r)^2+x(1+r)+x$ $\Big[$$x(1+r)+x$$\Big](1+r)+x$
$3$ $x(1+r)^3+x(1+r)^2+x(1+r)+x$ $\Big[$$x(1+r)^2+x(1+r)+x$$\Big](1+r)+x$

Comparing values across columns two and three of the above table, a convenient pattern emerges. For instance, the value in year 2, column three equals \(x\) plus \((1+r)\) times the value in year 1, column two.

If we want to calculate the value of your savings acount at the beginning of year \(55\), we need to tell Python to compound returns for \(55\) years. The really awful way to do this is to tell Python to calculate \(x(1+r)^{55}+x(1+r)^{54}+x(1+r)^{53}+...+x(1+r)^2+x(1+r)+x\). The programmer’s way to do this is to use the pattern we pointed out in the above table, and make use of a for loop that operates over a range of years.

If we wanted to loop over years \(0\) through \(3\), we could tell Python to use a for loop to iterate over a list of the numbers \(0\) through \(3\). A for loop has four components to it:

  • the word for : tells Python to execute a for loop

  • a variable name : this serves as a loop index. You’ll see in a moment what this index does. A common name used here is the letter i

  • the word in : tells Python that the loop index will be contained within ‘’something’’.

  • a list : this is the ‘’something’’ that we loop over. Every item in the list gets used once.

for i in [0, 1, 2, 3]:
    print(i)
0
1
2
3

What a for loop does is tells Python to loop over the list specified (in this case, the list of numbers \(0\) through \(4\)). For each item in that list, Python sets the loop index variable (in this case, we defined the index variable to be named i) equal to that value of the list and then executes every step within the for loop (all of the indented lines of code underneath the line for. Here, our for loop only specifies one action: print out the value of the loop index.

In our motivating example, we want Python to work over a long range of years. In this case, years \(0\) through \(55\). This would be tedious to type out explicitly into [0, 1, 2, ..., 54, 55]. Python has a trick for this, but there’s one caveat to keep in mind. As children, we learn to start counting with the number \(1\). Python, like most programming languages, starts counting with the number \(0\). Thus, the built-in function range() uses this starts-at-zero convention.

for t in range(5):
    print(t)
0
1
2
3
4

As demonstrated above, range(5) tells Python that the loop should iterate over numbers \(0\) through \(4\). Generally, we use range(n) to obtain numbers \(0\) through \(n-1\) (not \(0\) through \(n\)). Thus, we get \(n\) numbers out of range(n). If we want to iterate up through year \(55\), we would therefore use the statement range(56).

To calculate the value of your savings after \(55\) years, we simply have to type out:

savings = 0 # initially, 0 dollars in savings account
for i in range(56):
    savings = savings*(1+r) + x # each year, pre-existings savings grows by (1+r) and an additional x is added
print(savings)
2094111.7408149564

Concept check: Edit the following code so that the value for my_savings is printed out at years 10, 20, and 30. Hint: check for whether i is in [10, 20, 30], or use the % operator.

my_savings = 0
for i in range(35):
    my_savings = my_savings*(1+r) + x
    # edit the code here!
    
print(savings) # this will print the savings after 35 years, we want to see savings at year 10, 20, 30 as well
2094111.7408149564

For loops allow us to quickly visualize an idea. For example, let’s return to the valuation problem from earlier in the chapter.

def better_wacc(E, D, rE, rD, tC):
    
    if rE <= 0:
        raise Exception('rE is not positive')
    if rD <= 0:
        raise Exception('rD is not positive')
    if tC <= 0:
        raise Exception('tC is not positive')
    
    V = E + D
    cost_of_capital = E / V * rE + D / V * rD * (1-tC)
    return cost_of_capital

def cost_of_equity(E, D, rA, rD, tC):
    return rA + (rA-rD)*(1-tC)*(D/E)

def firm_value(E, D, rA, rD, tC, FCF, g):
    
    rE = cost_of_equity(E=E, D=D, rA=rA, rD=rD, tC=tC)
    rW = better_wacc(E, D, rE, rD, tC)
    
    if rW < g:
        raise Exception('rW must be greater than g, but g=', g, 'and rW=', rW)
    
    return FCF / (rW - g)

Earlier, we saw that the value of the firm changed as its leverage ratio changed. A more complete, easier to see representation of this idea is to loop over many different debt-equity mixes and check what happens to firm value.

Before jumping on the firm value example, let’s clarify the usages of range(). There are three possible ways to call the range() function:

  1. range(n): gives numbers \(0\) to \(n\), i.e. [0, 1, …, n]

  2. range(k,n): gives numbers \(k\) to \(n\), i.e. [k, k+1, …, n]

  3. range(k,n,b): gives numbers \(k\) to \(n\), incrementing by \(b\)

It’s perhaps easiest to understand the use cases with a few examples.

for i in range(7):
    print(i)
0
1
2
3
4
5
6
for i in range(2,7):
    print(i)
2
3
4
5
6
for i in range(2,7,3):
    print(i)
2
5

In the earlier example estimating firm value, the level of debt and equity of the firm added up to be 100. Let’s stick to the example numbers used earlier, and consider different debt-equity mixes that add to 100. We could iterate over possible debt levels of 0 to 90 with either a call to range(91) or range(0,91), but that would call the firm_value() function 91 times! That many calculated firm values would be hard to read. To make things easier on ourselves, let’s try debt values of 0, 10, 20, …, 90. We can do this with the three-input version of the range() function call.

for D in range(0, 100, 10):
    v = firm_value(E=100-D, D=D, rA=.05, rD=.04, tC=.35, FCF=2, g=.01)
    print(D, v)
0 50.0
10 52.287581699346404
20 54.7945205479452
30 57.55395683453237
40 60.6060606060606
50 64.0
60 67.79661016949152
70 72.07207207207207
80 76.92307692307692
90 82.4742268041237

Think carefully about why range(0,100,10) gives values [0, 10, 20, …, 80, 90] and not values [0, 10, 20, …, 90, 100].