loading...
Cover image for Understanding loops

Understanding loops

jamesbright profile image James Ononiwu ・7 min read

Loops are what makes programming such a powerful art. in fact without iteration a lot of problems coding solves today wouldn't have been possibly solved, it would have been a tedious task to write repetitive instructions for the number of times needed. this is why understanding what's happening under the hood when an iteration is going on is very important in making efficient use of loops to become a better programmer.

The simple part

simple use of a loop is the use of a single loop to print values contained in a list, or print characters that make up a string etc.

#print values in a list
countries = ['Germany','USA','Spain']
for country in countries: #iterate over countries list
    print(country)

#print characters that make up a string
country = 'Germany'
for letter in country: #iterate over countries list
    print(letter)

the above code is for simple cases and easy for a beginner to pick up.

Challenge most beginners face.

the real challenge comes when we have nested loops - that is a loop inside another loop. unless you are building a time machine, two nested loops are enough to solve most iteration problems. the number of nested loops is directly proportional to the efficiency of the code as the deeper the nesting the less efficient and more time required to run. let's combine the two examples above to form a nested loop.

#nested for loops
countries = ['Germany','USA','Spain']
for country in countries: #iterate over countries list
    print('beginning of outer loop') 
    print('{} is at index {} of {}'.format(country,countries.index(country), countries),'\n')
    print('beginning of inner loop for {}'.format(country))

    for letter in country: #iterate over each country in countries list
        print('{} is at index {} of {}'.format(letter,country.index(letter),country))
    print('end of inner loop for {}'.format(country),'\n')

print('end of outer loop')

running the code above will give the output below.

beginning of outer loop
Germany is at index 0 of ['Germany', 'USA', 'Spain'] 

beginning of inner loop for Germany
G is at index 0 of Germany
e is at index 1 of Germany
r is at index 2 of Germany
m is at index 3 of Germany
a is at index 4 of Germany
n is at index 5 of Germany
y is at index 6 of Germany
end of inner loop for Germany 

beginning of outer loop
USA is at index 1 of ['Germany', 'USA', 'Spain'] 

beginning of inner loop for USA
U is at index 0 of USA
S is at index 1 of USA
A is at index 2 of USA
end of inner loop for USA 

beginning of outer loop
Spain is at index 2 of ['Germany', 'USA', 'Spain'] 

beginning of inner loop for Spain
S is at index 0 of Spain
p is at index 1 of Spain
a is at index 2 of Spain
i is at index 3 of Spain
n is at index 4 of Spain
end of inner loop for Spain 

end of outer loop

The formating and explanation outputs are for a better understanding of what is going on.
the objective of the code is to print each country contained in the countries list with their index, and also print the characters that spell them. understanding loops have more to do with knowing how index changes with each iteration. from the example above each index range starts at 0 and ends at the length of the iterable -1, for instance, the count of values in countries list is 3, therefore iteration is of the range 0,1,2. for a nested loop, execution of the outer loop once, is followed by complete execution of the inner loop and then control is handed back to the outer loop to continue executing.


beginning of outer loop
Germany is at index 0 of ['Germany', 'USA', 'Spain'] 

beginning of inner loop for Germany
G is at index 0 of Germany
e is at index 1 of Germany
r is at index 2 of Germany
m is at index 3 of Germany
a is at index 4 of Germany
n is at index 5 of Germany
y is at index 6 of Germany
end of inner loop for Germany 

beginning of outer loop
USA is at index 1 of ['Germany', 'USA', 'Spain'] 

this is continued until the outer loop iteration reaches the end of the list in this case at the value Spain being the last value in the list.
Let's see another example of a nested loop where we need to check for duplicates in a sequence of numbers.

 def is_unique(S):
    """Return True if there are no duplicate elements in sequence S."""
    for j in range(len(S)):
        for k in range(j+1, len(S)):
            if S[j] == S[k]:
                print('S[{}] {}, S[{}] {}'.format(j,S[j],k,S[k]),'duplicate found')
                return False #found duplicate
            else:
                print('S[{}] {}, S[{}] {}'.format(j,S[j],k,S[k]))
    return True #if we reach this, elements were unique

Calling the function with the values..


result = is_unique([77,55,33,44,33])
print(result)

we will get the output below

S[0] 77, S[1] 55
S[0] 77, S[2] 33
S[0] 77, S[3] 44
S[0] 77, S[4] 33
S[1] 55, S[2] 33
S[1] 55, S[3] 44
S[1] 55, S[4] 33
S[2] 33, S[3] 44
S[2] 33, S[4] 33 duplicate found

False

Again, the output formatting is for you to better understand what is going on, but we did receive False showing that the list contains duplicate values which happen to be the number 33. as you can see from the output each iteration of the outer loop i.e S[0] is followed by a complete iteration of the inner loop.

S[0] 77, S[1] 55
S[0] 77, S[2] 33
S[0] 77, S[3] 44
S[0] 77, S[4] 33

S[1] 55, S[2] 33
S[1] 55, S[3] 44
S[1] 55, S[4] 33


S[2] 33, S[3] 44
S[2] 33, S[4] 33 duplicate found

But there's something new in the code, and that is the comparison of values inside the loop using the current iteration index.

  for j in range(len(S)):
        for k in range(j+1, len(S)):
            if S[j] == S[k]:

from the code above, you will understand that if the current index j of the outer loop is 0 then the first index k of the inner loop will be one because k starts at j+1, and that's why we have...

S[0] 77, S[1] 55

S[1] 55, S[2] 33

S[2] 33, S[3] 44

which helped us to compare value at index j S[j] to value at index j+1 S[j+1] or Sk to see if they are same.

Index out of range

Consider the code below that print each country contained in a list of countries

countries = ['Germany','USA','Spain']
country_index = 0
while country_index < len(countries):
    print(countries[country_index])
    country_index += 1 #increment index
Germany
USA
Spain

Notice that using while loops we have to specify a counter (in this case country_index) which is used as a checkpoint to terminate the loop if the condition no longer holds true, else we will have the loop running indefinitely.

Now we want to print the values of the same list in reverse order by trying the code below.

countries = ['Germany','USA','Spain']
countries_len = len(countries)
while countries_len >= 0:
    print(countries[countries_len])
    countries_len -= 1 #decrement index

after running the code, instead of getting what we want. we ended up with the output below.

 2 countries_len = len(countries)
      3 while countries_len >= 0:
----> 4     print(countries[countries_len])
      5     countries_len -= 1 #decrement index

IndexError: list index out of range

list index out of range error.
What causes this error is simply the fact that indexing in python starts at 0. we might say we specified that by doing..

while countries_len >= 0:

But that's not where the exception occurred. it happened at line 4.


----> 4     print(countries[countries_len])

Because we are trying to get a value with an index that is greater than the maximum index of the list. len(countries) will give you the value 3 which is the count of elements in the list starting at 1, but indexing in python starts at 0. so the range of index of countries list is 0,1,2 having maximum value as 2.

print(countries_len)

Gives the output.

3

while printing the indexes


for i in range(len(countries)):
    print(i)

gives.

0
1
2

to correct the error we simply subtract 1 from the length of the list.

countries = ['Germany','USA','Spain']
countries_len = len(countries)-1 #subtract 1 from length
while countries_len >= 0:
    print(countries[countries_len])
    countries_len -= 1 #decrement index

and we get the correct output.

Spain
USA
Germany

Another example of this error is gotten when we re-write the previous is_unique function this way.

def is_unique(S):
    """Return True if ther are no duplicate elements in sequence S."""
    sorted_S = sorted(S) # sort the sequence S.
    for j in range(1, len(sorted_S)):
        if sorted_S[j] == sorted_S[j+1]:
            return False
    return True

calling the function with the sequence below as argument..

numbers = [33,44,33,35,54,77]
result = is_unique(numbers)
print(result)

We will get the error output.

4     for j in range(1, len(sorted_S)):
----> 5         if sorted_S[j] == sorted_S[j+1]:
      6             return False
      7     return True

IndexError: list index out of range

Because we are trying to access values from an index j+1 which will not exist if we reach the maximum index j of the list.

To correct this error we simply change j+1 to j-1, this will have the list compare it's current value S[j] with it's previous value S[j-1],

def is_unique(S):
    """Return True if ther are no duplicate elements in sequence S."""
    sorted_S = sorted(S)
    for j in range(1, len(sorted_S)):
        if sorted_S[j] == sorted_S[j-1]: # changed j+1 to j-1
            return False
    return True

Executing

numbers = [33,44,33,35,54,77]
result = is_unique(numbers)
print(result)

returned the correct output.

False

we've seen what happens under the hood during an iteration, and also errors you might encounter and how to fix them. i hope this have helped you in understanding a loop better. Thanks for reading.

Posted on by:

jamesbright profile

James Ononiwu

@jamesbright

full-stack web developer and data scientist. interested in learning new things and documenting my journey by writing articles.

Discussion

markdown guide
 

I always advise against nesting loops at all cost. In real projects it quickly gets out of control and you end up 5+ tab stops out. I recommend treating items differently than their containers. Below I have refactored your first example using this methodology, keeping most of your code exactly the same.

# Un-nested for loops

def print_letters_from_countries(countries):
    "prints each letter for each country in a list of countries"
    for country in countries: #iterate over countries list
        print('beginning of outer loop') 
        print('{} is at index {} of {}'.format(country,countries.index(country), countries),'\n')
        print('beginning of inner loop for {}'.format(country))
        print_letters_from_country(country)

def print_letters_from_country(country):
    "prints each letter of a single country"
    for letter in country: #iterate over each country in countries list
        print('{} is at index {} of {}'.format(letter,country.index(letter),country))
    print('end of inner loop for {}'.format(country),'\n')


countries = ['Germany','USA','Spain']
print_letters_from_countries(countries)

Output

beginning of outer loop
Germany is at index 0 of ['Germany', 'USA', 'Spain']

beginning of inner loop for Germany
G is at index 0 of Germany
e is at index 1 of Germany
r is at index 2 of Germany
m is at index 3 of Germany
a is at index 4 of Germany
n is at index 5 of Germany
y is at index 6 of Germany
end of inner loop for Germany

beginning of outer loop
USA is at index 1 of ['Germany', 'USA', 'Spain']

beginning of inner loop for USA
U is at index 0 of USA
S is at index 1 of USA
A is at index 2 of USA
end of inner loop for USA

beginning of outer loop
Spain is at index 2 of ['Germany', 'USA', 'Spain']

beginning of inner loop for Spain
S is at index 0 of Spain
p is at index 1 of Spain
a is at index 2 of Spain
i is at index 3 of Spain
n is at index 4 of Spain
end of inner loop for Spain
 

You can also get the index in a more pythonic fashion by using the enumerate function to get the index while looping. This version, uses enumerate and f-strings.

# Un-nested for loops

def print_letters_from_countries(countries):
    for i, country in enumerate(countries): #iterate over countries list
        print('beginning of outer loop') 
        print(f'{country} is at index {i} of {countries}\n')
        print(f'beginning of inner loop for {country}')
        print_letters_from_country(country)

def print_letters_from_country(country):
    for i, letter in enumerate(country): #iterate over each country in countries list
        print(f'{letter} is at index {i} of {country}')
    print(f'end of inner loop for {country}\n')


countries = ['Germany','USA','Spain']
print_letters_from_countries(countries)
 
 

Thanks for the recommendation

 

To really understand loops it is important to understand that they are recursive. :)