Nested loops

Nested loops are loops within loops. We already saw them in the context of the Missing Number problem. Here is a simpler example.

In [1]:
counter = 0 #the number of times we printed output so far
for i in range(5):      
    for j in range(3):  
        print(counter, ': i = ',  i, ', j = ', j, sep = "")
        counter += 1
0: i = 0, j = 0
1: i = 0, j = 1
2: i = 0, j = 2
3: i = 1, j = 0
4: i = 1, j = 1
5: i = 1, j = 2
6: i = 2, j = 0
7: i = 2, j = 1
8: i = 2, j = 2
9: i = 3, j = 0
10: i = 3, j = 1
11: i = 3, j = 2
12: i = 4, j = 0
13: i = 4, j = 1
14: i = 4, j = 2

(sep="") removes the spaces between the different things that are printed.

Here is one way to understand what's going on here. The outer loop set i to 0, 1, 2, 3, 4. Then, for each of those i's, the inner loop is run:

#i is fixed
for j in range(3):  
    print(counter, ': i = ',  i, ', j = ', j, sep = "")
    counter += 1

For example, for i=2, the loop that is running is, in effect, for j in range(3):
print(counter, ': i = ', 2, ', j = ', j, sep = "") counter += 1

So that the output (ignoring the couter) would be i = 2, j = 0 i = 2, j = 1 i = 2, j = 2

This process is repeated for every i.

In addition, every time we print, the variable counter is incremented by 1. That means that counter will go from 0, to 1, to 2, ... when we print.

At the end, the value of counter is

In [2]:
counter
Out[2]:
15

That's because we repeated print 3 times inside the inner loop, and we repeated the whole thing 5 times, so we printed output $15 = 5\times 3$ times in total.

Unrolling the loop

Here is another way to understand the loop. Here is some equivalent code:

i = 0        
for j in range(3):
    print(counter, ": i = ", i, ", j = ", j, sep = "")
    counter += 1

i = 1        
for j in range(3):
    print(counter, ": i = ", i, ", j = ", j, sep = "")
    counter += 1

i = 2
for j in range(3):
    print(counter, ": i = ", i, ", j = ", j, sep = "")
    counter += 1
#....

Converting a nested loop to a nested loop calling a helper function

Like before, we can convert the nested loop to a simple loop that calls a helper function:

In [3]:
def count_i(i, n_times):
    global counter
    for j in range(n_times):
        counter += 1
        print(counter,": i = ", i, ", j = ", j)

if __name__ == '__main__':
    counter = 0
    for i in range(5):
        count_i(i, 3)
1 : i =  0 , j =  0
2 : i =  0 , j =  1
3 : i =  0 , j =  2
4 : i =  1 , j =  0
5 : i =  1 , j =  1
6 : i =  1 , j =  2
7 : i =  2 , j =  0
8 : i =  2 , j =  1
9 : i =  2 , j =  2
10 : i =  3 , j =  0
11 : i =  3 , j =  1
12 : i =  3 , j =  2
13 : i =  4 , j =  0
14 : i =  4 , j =  1
15 : i =  4 , j =  2

The code is the same. The difference is that we made a helper function instead of having an inner loop. We used a global counter variable here. The alternative would be to pass counter as a parameter, and then return what the counter is at the end from count_i. Neither solution is ideal (global variables are to be avoided generally if possible, and passing counter back and forth is a little bit awkward). Really, a nested loop without a helper function is probably appropriate here.

Printing all passwords

Let's now print all words of length 2 that contain letters from alphabet = ["a", "b", "c", "d", "e", "f"] This is a somewhat similar task to what we did before: above, we are printing all two-digit combinations of the form [digit between 0 and 4][digits between 0 and 2]

Here is how we can print all passwords of length 2

In [4]:
alphabet = ["a", "b", "c", "d", "e", "f"]
for letter1 in alphabet:
    for letter2 in alphabet:
        print(letter1 + letter2)
aa
ab
ac
ad
ae
af
ba
bb
bc
bd
be
bf
ca
cb
cc
cd
ce
cf
da
db
dc
dd
de
df
ea
eb
ec
ed
ee
ef
fa
fb
fc
fd
fe
ff

The story is the same: we fix letter1 to be, in turn, "a", "b", "c", ..., and then go over all the possibility for letter2.

So for example when letter1 is "c", the inner loop prints ca cb cc cd ce cf

How about all possible words of length 3?

In [5]:
alphabet = ["a", "b", "c", "d"]
for letter1 in alphabet:
    for letter2 in alphabet:
        for letter3 in alphabet:
            print(letter1 + letter2 + letter3)
aaa
aab
aac
aad
aba
abb
abc
abd
aca
acb
acc
acd
ada
adb
adc
add
baa
bab
bac
bad
bba
bbb
bbc
bbd
bca
bcb
bcc
bcd
bda
bdb
bdc
bdd
caa
cab
cac
cad
cba
cbb
cbc
cbd
cca
ccb
ccc
ccd
cda
cdb
cdc
cdd
daa
dab
dac
dad
dba
dbb
dbc
dbd
dca
dcb
dcc
dcd
dda
ddb
ddc
ddd

To understand how this works, again think from the bottom up. In the innermost loop, letter1+letter2 is fixed, and we go through all the possible letter3's. For example, if letter1+letter2 is "ca", we print out

caa
cab
cac



In the second-most inner loop, letter1 is fixed, and we go through all the possible letter2's, and for each letter2 we go through all the possible letter3's.