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.

Looping over a list of integers using range()#

Sometimes we want to simply loop over a list of integers, e.g., to calculate the first five squared integers we might write:

for i in [1, 2, 3, 4, 5]:
    print(i**2)
1
4
9
16
25

or if we want to store these squares in a new list:

squares = []
for i in [1, 2, 3, 4, 5]:
    squares.append(i**2)
print(squares)
[1, 4, 9, 16, 25]

To generate a sequence of regularly spaced integers we can use the built-in function range. For example, to simplify the code above:

squares = []
for i in range(1, 6): # loop over i = 1 … 5
    squares.append(i**2)
print(squares)
[1, 4, 9, 16, 25]

When range is passed two arguments, like this, the syntax is range(start, stop+1). We can also use range with a single argument, in which case it starts from 0.

squares = []
for i in range(6): # loop over i = 0 … 5
    squares.append(i**2)
print(squares)
[0, 1, 4, 9, 16, 25]

You can also specify a step-size different from 1 by passing an optional third argument:

squares = []
for i in range(0, 10, 2): # loop over even i = 0 … 10
    squares.append(i**2)
print(squares)
[0, 4, 16, 36, 64]

The step size can even be a negative value:

squares = []
for i in range(10, 0, -1): # loop over i = 10 … 1
    squares.append(i**2)
print(squares)
[100, 81, 64, 49, 36, 25, 16, 9, 4, 1]

List comprehensions#

In the examples 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 0x7f0c387d3440>

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

while loops#

In addition to for loops, Python also provides while loops for situations where you want to repeat a block of code as long as a condition is true.

concentration = 1.0
equilibrium_constant = 0.1
iteration = 0

while concentration > equilibrium_constant:
    concentration *= 0.9  # Decrease by 10% each iteration
    iteration += 1
    print(f"Iteration {iteration}: Concentration = {concentration:.4f}")

print(f"Equilibrium reached after {iteration} iterations")
Iteration 1: Concentration = 0.9000
Iteration 2: Concentration = 0.8100
Iteration 3: Concentration = 0.7290
Iteration 4: Concentration = 0.6561
Iteration 5: Concentration = 0.5905
Iteration 6: Concentration = 0.5314
Iteration 7: Concentration = 0.4783
Iteration 8: Concentration = 0.4305
Iteration 9: Concentration = 0.3874
Iteration 10: Concentration = 0.3487
Iteration 11: Concentration = 0.3138
Iteration 12: Concentration = 0.2824
Iteration 13: Concentration = 0.2542
Iteration 14: Concentration = 0.2288
Iteration 15: Concentration = 0.2059
Iteration 16: Concentration = 0.1853
Iteration 17: Concentration = 0.1668
Iteration 18: Concentration = 0.1501
Iteration 19: Concentration = 0.1351
Iteration 20: Concentration = 0.1216
Iteration 21: Concentration = 0.1094
Iteration 22: Concentration = 0.0985
Equilibrium reached after 22 iterations

Simple Loop Control Statements#

Python provides break and continue statements for additional control in loops. Here are some simple examples to demonstrate their use:

Using break#

The break statement allows you to exit a loop prematurely. Here’s a simple example:

# Print numbers until we reach 5
for number in range(1, 10):
    print(number)
    if number == 5:
        break

print("Loop ended")
1
2
3
4
5
Loop ended

In this example, the loop prints numbers from 1 to 9, but we use break to exit the loop when we reach 5.

Using continue#

The continue statement allows you to skip the rest of the current iteration and move to the next one. Here’s a simple example:

# Print odd numbers only
for number in range(1, 10):
    if number % 2 == 0:
        continue
    print(number)

print("Loop ended")
1
3
5
7
9
Loop ended

In this example, we use continue to skip even numbers, effectively printing only odd numbers.

Exercises#

  1. Modify the chemical reaction simulation (while loop example) to stop if the reaction has not reached equilibrium after 100 iterations. Use a break statement to exit the loop in this case.

  2. Write a loop that calculates the factorial of a number.

  3. Given a list of atomic masses and a list of stoichiometric coefficients, write a loop (or list comprehension) that calculates the molecular mass of a compound.

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

Hints for Exercise 4:#

  1. Recall that in the Lists exercise 2, you were given the coordinates of three atoms for two different molecules.

  2. To calculate distances between all pairs of atoms, you’ll need two loops:

    • An outer loop to iterate over each atom.

    • An inner loop to iterate over all other atoms to pair with the current atom from the outer loop.

  3. Remember the distance formula: \(r_{12} = \sqrt{(x_2-x_1)^2 + (y_2-y_1)^2 + (z_2-z_1)^2}\)

  4. You can use list indexing to access the \(x\), \(y\), and \(z\) coordinates of each atom. For example, if atom1 = [0.1, 0.5, 3.2], then:

    • atom1[0] is the \(x\)-coordinate

    • atom1[1] is the \(y\)-coordinate

    • atom1[2] is the \(z\)-coordinate

  5. You might find it helpful to store all three atoms in a single list, like this:

    molecule = [atom_1, atom_2, atom_3]
    

    This makes it easier to loop over the atoms.

  6. Consider using enumerate() in your outer loop to keep track of which atoms you are calculating the distance between.

  7. Print out which atoms you’re calculating the distance between along with the result, to make your output clear and readable.

  8. To avoid calculating the distance of an atom with itself or calculating the same pair twice, start the inner loop from the index after the current atom in the outer loop.