Understanding loops
Tags: python, beginners, loops
Published: 2020-08-01 23:56:45

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.
```python
#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.
```python
#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.

```python
 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..
```python

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.
```python
  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 S[k] (since k=j+1) to see if they are same.

### Index out of range
Consider the code below that print each country contained in a list of countries
```python
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.
```python
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..
```python
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.

```python
print(countries_len)
```
Gives the output.
```
3
```
while printing the indexes
```python

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.
```python
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.

```python
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..
```python
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]**,

```python
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
```python
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.