Loops

A common use for lists is to store a homogeneous set of data (meaning, each element has the same type). For example, we might store a set of reactant concentrations recorded at different times:

reactant_concentrations = [5.0, 4.0, 3.0, 2.0, 1.0]

We may want to perform the same calculation on each element of a list; for example, calculating the square of each concentration, and then store these results in a new *list.

We could do this using list indexing, e.g.

squared_concentrations = [reactant_concentrations[0]**2, 
                          reactant_concentrations[1]**2,
                          reactant_concentrations[2]**2,
                          reactant_concentrations[3]**2,
                          reactant_concentrations[4]**2]
print(squared_concentrations)
[25.0, 16.0, 9.0, 4.0, 1.0]

Note that the new list created on the right hand side has been written split over multiple line.

Python ignores the end of a line within pairs of brackets [], (), or {}.

This approach is clunky, and error-prone, and does not scale (imagine if we had 100 values in our dataset!).

Looping over elements of a list using for

We can loop over every element of a list (or indeed of any object that represents an ordered set of values), performing the same operation on each element in turn, using a for loop.

A for loop has the following general syntax

for variable in iterable:
    do something

For example, our calculation above to get the squared concentrations might be rewritten as

for c in reactant_concentrations:
    print(c**2)
25.0
16.0
9.0
4.0
1.0

The first line defines the start of the loop:

  • in reactant_concentrations:: We are going to loop over each element in turn of the list reactant_concentrations.

  • for c: At each iteration of the loop, we will assign the variable c to the corresponding element in reactant_concentrations

The first line finishes with a colon :.

After the first line we define what will happen inside the loop. The code inside a loop is denoted by being indented by a fixed amount of whitespace; in this case, four spaces.

Indentation plays a very important in Python, because it marks out where blocks of code, such as the code inside loops, start and finish.

When we run this code, for the first iteration, the variable c is assigned to the first value stored in reactant_concentration. This is equivalent to writing

c = reactant_concentration[0]

The line of code inside the loop uses this value of c to execute the print() statement.

We then perform the second iteration: now the variable c is assigned to the second value stored in reactant_concentration. This is equivalent to

c = reactant_concentration[1]

The line of code inside the loop uses this value of c to execute the print() statement.

This is repeated, each time assigning c to the next element in the reactant_concentration list.

Once each element in reactant_concentration has been processed, we exit the loop.

The end of a loop is indicated by removing the previous indentation e.g.

for c in reactant_concentrations:
    print('Inside the loop')
    print(c**2)
print('Outside the loop')
Inside the loop
25.0
Inside the loop
16.0
Inside the loop
9.0
Inside the loop
4.0
Inside the loop
1.0
Outside the loop

Note that the variable c can still be used after the loop has finished. Furthermore, if you were using a variable c before this loop, the value will be overwritten:

c = '32.0'
print(f'c = {c}')
for c in reactant_concentrations:
    print(c**2)
print('finished the loop')
print(f'c = {c}')
c = 32.0
25.0
16.0
9.0
4.0
1.0
finished the loop
c = 1.0

To use a for loop to calculate the squared concentrations and store them in a new list, we can use append() for each iteration of the loop.

squared_concentrations = []
for c in reactant_concentrations:
    squared_concentrations.append(c**2)
print(squared_concentrations)
[25.0, 16.0, 9.0, 4.0, 1.0]

Here we create and empty list on the first line. Then each iteration of the loop calculates the square of the relevant element of reactant_concentrations, and uses append() to add this to the end of squared_concentrations.

By adding a print() statement inside the loop, we can see how this builds up the final squared_concentrations list:

squared_concentrations = []
for c in reactant_concentrations:
    squared_concentrations.append(c**2)
    print('In the loop')
    print(f'squared_concentrations = {squared_concentrations}')
print('Outside the loop')
print(squared_concentrations)
In the loop
squared_concentrations = [25.0]
In the loop
squared_concentrations = [25.0, 16.0]
In the loop
squared_concentrations = [25.0, 16.0, 9.0]
In the loop
squared_concentrations = [25.0, 16.0, 9.0, 4.0]
In the loop
squared_concentrations = [25.0, 16.0, 9.0, 4.0, 1.0]
Outside the loop
[25.0, 16.0, 9.0, 4.0, 1.0]

Enumerating loop iterations

Sometimes we want to track which iteration of a loop we are on, while we are going through it. One way to do this is to add another variable that we use to track the numbers of iterations:

squared_concentrations = []
i = 0
for c in reactant_concentrations:
    squared_concentrations.append(c**2)
    print(f'In the loop, iteration {i}')
    print(f'squared_concentrations = {squared_concentrations}')
    i = i + 1
print('Outside the loop')
print(squared_concentrations)
In the loop, iteration 0
squared_concentrations = [25.0]
In the loop, iteration 1
squared_concentrations = [25.0, 16.0]
In the loop, iteration 2
squared_concentrations = [25.0, 16.0, 9.0]
In the loop, iteration 3
squared_concentrations = [25.0, 16.0, 9.0, 4.0]
In the loop, iteration 4
squared_concentrations = [25.0, 16.0, 9.0, 4.0, 1.0]
Outside the loop
[25.0, 16.0, 9.0, 4.0, 1.0]

A better way to do this is to use the enumerate() function:

squared_concentrations = []
for i, c in enumerate(reactant_concentrations):
    squared_concentrations.append(c**2)
    print(f'In the loop, iteration {i}')
    print(f'squared_concentrations = {squared_concentrations}')
print('Outside the loop')
print(squared_concentrations)
In the loop, iteration 0
squared_concentrations = [25.0]
In the loop, iteration 1
squared_concentrations = [25.0, 16.0]
In the loop, iteration 2
squared_concentrations = [25.0, 16.0, 9.0]
In the loop, iteration 3
squared_concentrations = [25.0, 16.0, 9.0, 4.0]
In the loop, iteration 4
squared_concentrations = [25.0, 16.0, 9.0, 4.0, 1.0]
Outside the loop
[25.0, 16.0, 9.0, 4.0, 1.0]

The enumerate() function effectively converts a simple list such as reactant_concentrations into a sequence of pairs of values, i.e.

((0, 5.0), (1, 4.0), (2, 3.0), (3, 2.0), (1, 1.0))

Now each iteration of the loop assigns i and c to their respective values.

List comprehensions

In the example above, we started with a list of values, looped over every element and performed an operation on that element (squaring it) and then stored the results, in order, in a new list. This is a commmon programming pattern, and Python provides a shorthand syntax for doing this, called list comprehensions.

The general syntax of a list comprehension is

new_list = [expression for element in iterable]

Compare this to the equivalent for loop construction:

new_list = []
for element in iterable:
    new_list.append(expression)

Our example of computing squared reactant concentrations can be rewritten using a list comprehension as

squared_concentrations = [c**2 for c in reactant_concentrations]
print(squared_concentrations)
[25.0, 16.0, 9.0, 4.0, 1.0]

Nested loops

We saw previously that lists can be nested, i.e. a list may contain one of more sublists e.g.

my_list = [[1, 2, 3], [4, 5, 6], [7, 8, 9]]
print(my_list)
print(my_list[1])
print(my_list[1][2])
[[1, 2, 3], [4, 5, 6], [7, 8, 9]]
[4, 5, 6]
6

If we loop over the elements of this nested list, we get each sublist in turn:

for i, sublist in enumerate(my_list):
    print(i, sublist)
0 [1, 2, 3]
1 [4, 5, 6]
2 [7, 8, 9]

We can loop over each element in each sublist by writing a second loop inside the first loop (giving us a pair of nested loops):

for i, sublist in enumerate(my_list):
    for j, element in enumerate(sublist):
        print(f'outer loop iteration: {i}')
        print(f'inner loop iteration: {j}')
        print(f'element = {element}')
outer loop iteration: 0
inner loop iteration: 0
element = 1
outer loop iteration: 0
inner loop iteration: 1
element = 2
outer loop iteration: 0
inner loop iteration: 2
element = 3
outer loop iteration: 1
inner loop iteration: 0
element = 4
outer loop iteration: 1
inner loop iteration: 1
element = 5
outer loop iteration: 1
inner loop iteration: 2
element = 6
outer loop iteration: 2
inner loop iteration: 0
element = 7
outer loop iteration: 2
inner loop iteration: 1
element = 8
outer loop iteration: 2
inner loop iteration: 2
element = 9

We can also write a nested loop so that code is only executed as part of the outer loop, e.g.

for i, sublist in enumerate(my_list):
    print(f"In the outer loop: iteration {i}")
    for j, element in enumerate(sublist):
        print(f"    In the inner loop: iteration {j}")
        print(f"    element = {element}")
    print(f"In the outer loop again: iteration {i}")
In the outer loop: iteration 0
    In the inner loop: iteration 0
    element = 1
    In the inner loop: iteration 1
    element = 2
    In the inner loop: iteration 2
    element = 3
In the outer loop again: iteration 0
In the outer loop: iteration 1
    In the inner loop: iteration 0
    element = 4
    In the inner loop: iteration 1
    element = 5
    In the inner loop: iteration 2
    element = 6
In the outer loop again: iteration 1
In the outer loop: iteration 2
    In the inner loop: iteration 0
    element = 7
    In the inner loop: iteration 1
    element = 8
    In the inner loop: iteration 2
    element = 9
In the outer loop again: iteration 2

Looping over two lists in parallel

Sometimes we want to loop over two or more lists in parallel. For example, imagine we want to match up the nobel gases with their atomic numbers:

nobel_gases = ["Helium", "Neon", "Argon", "Krypton", "Radon"]
atomic_numbers = [2, 10, 18, 36, 54, 86]

We can create a sequence of pairs from two lists using the zip() function.

zip(nobel_gases, atomic_numbers)
<zip at 0x7fcee02a5550>

This has given us a zip object, which represents a sequence of pairs from our two original lists.

type(zip(nobel_gases, atomic_numbers))
zip

We can see how this pairing has worked by converting the zip object into another list:

list(zip(nobel_gases, atomic_numbers))
[('Helium', 2), ('Neon', 10), ('Argon', 18), ('Krypton', 36), ('Radon', 54)]

This is similar to the result of using enumerate() we saw earlier, which we could similarly convert to a list.

list(enumerate(nobel_gases))
[(0, 'Helium'), (1, 'Neon'), (2, 'Argon'), (3, 'Krypton'), (4, 'Radon')]

When using enumerate() we were able to simultaneously iterate over the list index and the corresponding list element using

for i, element in enumerate(nobel_gases):
    …

We can iterate over the pairs of values created by the zip() function in a similar way:

for name, a in zip(nobel_gases, atomic_numbers):
    print(f'The atomic mass of {name} is {a}')
The atomic mass of Helium is 2
The atomic mass of Neon is 10
The atomic mass of Argon is 18
The atomic mass of Krypton is 36
The atomic mass of Radon is 54

Exercise

Rewrite or modify your code from the Lists exercise 2 to calculate the distances between each pair of atoms, using a nested loop.

Worked Example