Worked Examples: Loops#

These worked solutions correspond to the exercises on the Loops page.

How to use this notebook:

  • Try each exercise yourself first before looking at the solution

  • The code cells show both the code and its output

  • Download this notebook if you want to run and experiment with the code yourself

  • Your solution might look different - that’s fine as long as it gives the correct answer!


Exercise 1: Factorial Calculation#

Problem: Write a for loop using range() that calculates the factorial of a given number.

The factorial of a positive integer \(n\) (written as \(n!\)) is the product of all positive integers less than or equal to \(n\). For example:

\[5! = 5 \times 4 \times 3 \times 2 \times 1 = 120\]

Solution

We’ll test the code with three cases: \(n = 5\), \(n = 10\), and \(n = 0\).

# Test case 1: n = 5 (should give 120)
n = 5
result = 1

for i in range(1, n + 1):
    result = result * i

print(f"{n}! = {result}")
5! = 120

Explanation:

  • We start with result = 1 because multiplying by 1 doesn’t change the value, and we need a starting point

  • range(1, n + 1) generates the numbers 1, 2, 3, …, n

  • Each iteration multiplies result by the current value of i

  • For \(n = 5\): we multiply \(1 \times 1 \times 2 \times 3 \times 4 \times 5 = 120\)

# Test case 2: n = 10 (should give 3,628,800)
n = 10
result = 1

for i in range(1, n + 1):
    result = result * i

print(f"{n}! = {result:,}")  # :, adds commas for readability
10! = 3,628,800
# Test case 3: n = 0 (should give 1 by convention)
n = 0
result = 1

for i in range(1, n + 1):
    result = result * i

print(f"{n}! = {result}")
0! = 1

Note on the special case \(0! = 1\):

When \(n = 0\), the range range(1, 1) produces no numbers, so the loop body never executes. The result remains at its initial value of 1, which is correct because by mathematical convention, \(0! = 1\).


Exercise 2: Molecular Mass Calculation#

Problem: Calculate the molecular mass of a compound by summing the products of each element’s atomic mass and its stoichiometric coefficient.

Solution Method 1: Using a for loop with range()

# Test case 1: Water (H2O)
atomic_masses = [1.008, 15.999]  # H, O
stoichiometric_coefficients = [2, 1]  # H2 + O

molecular_mass = 0
for i in range(len(atomic_masses)):
    molecular_mass += atomic_masses[i] * stoichiometric_coefficients[i]

print(f"Molecular mass of H2O: {molecular_mass:.3f} u")
Molecular mass of H2O: 18.015 u

Explanation:

  • We start with molecular_mass = 0

  • We loop through the indices using range(len(atomic_masses))

  • For each index i, we access both atomic_masses[i] and stoichiometric_coefficients[i]

  • We add the product of mass × coefficient to the running total

  • += is shorthand for molecular_mass = molecular_mass + ...

Solution Method 2: Using zip() in a for loop

# Test case 1: Water (H2O) using zip
atomic_masses = [1.008, 15.999]  # H, O
stoichiometric_coefficients = [2, 1]  # H2 + O

molecular_mass = 0
for mass, coeff in zip(atomic_masses, stoichiometric_coefficients):
    molecular_mass += mass * coeff

print(f"Molecular mass of H2O: {molecular_mass:.3f} u")
Molecular mass of H2O: 18.015 u

Explanation:

  • zip() pairs up corresponding elements from both lists: (1.008, 2) and (15.999, 1)

  • We can unpack these pairs directly in the for loop: for mass, coeff in zip(...)

  • This is cleaner than using explicit indexing because we don’t need to manage indices

Solution Method 3: Using zip() and a list comprehension

# Test case 1: Water (H2O) using zip and list comprehension
atomic_masses = [1.008, 15.999]  # H, O
stoichiometric_coefficients = [2, 1]  # H2 + O

molecular_mass = sum([mass * coeff for mass, coeff in zip(atomic_masses, stoichiometric_coefficients)])

print(f"Molecular mass of H2O: {molecular_mass:.3f} u")
Molecular mass of H2O: 18.015 u

Explanation:

  • We’re using zip() again to pair up corresponding elements from both lists

  • The list comprehension [mass * coeff for mass, coeff in zip(...)] iterates over these pairs

  • For each pair (mass, coeff), we calculate the product mass * coeff

  • This creates a list of products: [2.016, 15.999] for water

  • sum() adds all the products together

  • This is the most concise solution, combining everything into a single line

Testing with glucose (C6H12O6)#

# Test case 2: Glucose (C6H12O6)
atomic_masses = [12.011, 1.008, 15.999]  # C, H, O
stoichiometric_coefficients = [6, 12, 6]  # C6 + H12 + O6

molecular_mass = sum([mass * coeff for mass, coeff in zip(atomic_masses, stoichiometric_coefficients)])

print(f"Molecular mass of C6H12O6: {molecular_mass:.3f} u")
Molecular mass of C6H12O6: 180.156 u

Testing with sulphuric acid (H2SO4)#

# Test case 3: Sulphuric acid (H2SO4)
atomic_masses = [1.008, 32.065, 15.999]  # H, S, O
stoichiometric_coefficients = [2, 1, 4]  # H2 + S1 + O4

molecular_mass = sum([mass * coeff for mass, coeff in zip(atomic_masses, stoichiometric_coefficients)])

print(f"Molecular mass of H2SO4: {molecular_mass:.3f} u")
Molecular mass of H2SO4: 98.077 u

Exercise 3: Interatomic Distances with Nested Loops#

Problem: Calculate distances between all atoms in an ammonia (NH₃) molecule using nested loops.

The distance between two atoms is:

\[r_{ij} = \sqrt{(x_i - x_j)^2 + (y_i - y_j)^2 + (z_i - z_j)^2}\]

First, let’s set up the atomic coordinates and import the sqrt function.

from math import sqrt

# Ammonia (NH3) atomic coordinates in Ångströms
atom_N = [0.0, 0.0, 0.0]       # Nitrogen at origin
atom_H1 = [0.0, 0.94, 0.38]    # Hydrogen 1
atom_H2 = [0.81, -0.47, 0.38]  # Hydrogen 2  
atom_H3 = [-0.81, -0.47, 0.38] # Hydrogen 3

atoms = [atom_N, atom_H1, atom_H2, atom_H3]
atom_labels = ['N', 'H1', 'H2', 'H3']

Part A: All Pairwise Distances (Including Redundant Pairs)#

This solution calculates all 16 distances (4 × 4), including:

  • Distances of atoms to themselves (which are 0)

  • Each pair twice (e.g., both N \(\to\) H1 and H1 \(\to\) N)

# Part A: Calculate all pairwise distances
print("Part A: All pairwise distances (including redundant pairs)")
print("=" * 58)

for i in range(len(atoms)):
    for j in range(len(atoms)):
        # Calculate distance between atom i and atom j
        dx = atoms[i][0] - atoms[j][0]
        dy = atoms[i][1] - atoms[j][1]
        dz = atoms[i][2] - atoms[j][2]
        
        distance = sqrt(dx**2 + dy**2 + dz**2)
        
        print(f"Distance from {atom_labels[i]:3s} to {atom_labels[j]:3s}: {distance:.4f} Å")
Part A: All pairwise distances (including redundant pairs)
==========================================================
Distance from N   to N  : 0.0000 Å
Distance from N   to H1 : 1.0139 Å
Distance from N   to H2 : 1.0106 Å
Distance from N   to H3 : 1.0106 Å
Distance from H1  to N  : 1.0139 Å
Distance from H1  to H1 : 0.0000 Å
Distance from H1  to H2 : 1.6261 Å
Distance from H1  to H3 : 1.6261 Å
Distance from H2  to N  : 1.0106 Å
Distance from H2  to H1 : 1.6261 Å
Distance from H2  to H2 : 0.0000 Å
Distance from H2  to H3 : 1.6200 Å
Distance from H3  to N  : 1.0106 Å
Distance from H3  to H1 : 1.6261 Å
Distance from H3  to H2 : 1.6200 Å
Distance from H3  to H3 : 0.0000 Å

Explanation:

  • The outer loop iterates through each atom as the starting point (index i)

  • The inner loop iterates through each atom as the ending point (index j)

  • We calculate the differences in each coordinate: dx, dy, dz

  • Then apply the distance formula: \(\sqrt{dx^2 + dy^2 + dz^2}\)

  • The formatting {atom_labels[i]:3s} ensures labels are padded to 3 characters for neat alignment

Note: You should see 16 total distances, with each distance between different atoms calculated twice.

Part B: Unique Distances Only#

Now we’ll modify the loops to calculate only the 6 unique pairwise distances.

The key insight: For each atom i, we only need to calculate distances to atoms with indices j > i.

# Part B: Calculate only unique pairwise distances
print("\nPart B: Unique pairwise distances only")
print("=" * 38)

for i in range(len(atoms)):
    for j in range(i + 1, len(atoms)):  # Start from i+1 to avoid redundancy
        # Calculate distance between atom i and atom j
        dx = atoms[i][0] - atoms[j][0]
        dy = atoms[i][1] - atoms[j][1]
        dz = atoms[i][2] - atoms[j][2]
        
        distance = sqrt(dx**2 + dy**2 + dz**2)
        
        print(f"Distance from {atom_labels[i]:3s} to {atom_labels[j]:3s}: {distance:.4f} Å")
Part B: Unique pairwise distances only
======================================
Distance from N   to H1 : 1.0139 Å
Distance from N   to H2 : 1.0106 Å
Distance from N   to H3 : 1.0106 Å
Distance from H1  to H2 : 1.6261 Å
Distance from H1  to H3 : 1.6261 Å
Distance from H2  to H3 : 1.6200 Å

Explanation of range(i + 1, len(atoms)):

  • When i = 0 (atom N): j loops over 1, 2, 3 (atoms H1, H2, H3)

  • When i = 1 (atom H1): j loops over 2, 3 (atoms H2, H3) — we already calculated N-H1

  • When i = 2 (atom H2): j loops over 3 (atom H3) — we already calculated N-H2 and H1-H2

  • When i = 3 (atom H3): the inner loop doesn’t execute — all pairs involving H3 have been calculated

This gives us exactly 6 unique distances:

  • 3 N-H bonds (all should be approximately equal for NH₃)

  • 3 H-H distances (all should be approximately equal)

Alternative Solution: Using enumerate() with zip()#

Here is an alternative approach that uses zip to loop over atoms and their labels at the same time, and avoids having to index the atom_labels and atoms lists inside the inner loop.

# Alternative: Using zip to avoid indexing
print("\nAlternative solution using zip:")
print("=" * 60)

for i, (label_i, atom_i) in enumerate(zip(atom_labels, atoms)):
    for label_j, atom_j in zip(atom_labels[i + 1:], atoms[i + 1:]):
        # Calculate distance between atom_i and atom_j
        dx = atom_i[0] - atom_j[0]
        dy = atom_i[1] - atom_j[1]
        dz = atom_i[2] - atom_j[2]
        
        distance = sqrt(dx**2 + dy**2 + dz**2)
        
        print(f"Distance from {label_i:3s} to {label_j:3s}: {distance:.4f} Å")
Alternative solution using zip:
============================================================
Distance from N   to H1 : 1.0139 Å
Distance from N   to H2 : 1.0106 Å
Distance from N   to H3 : 1.0106 Å
Distance from H1  to H2 : 1.6261 Å
Distance from H1  to H3 : 1.6261 Å
Distance from H2  to H3 : 1.6200 Å

Explanation:

The key difference is using zip() to pair labels with coordinates, so we can work directly with the values instead of using indices.

First solution:

  • Uses indices i and j throughout: atoms[i], atoms[j], atom_labels[i], atom_labels[j]

Alternative solution:

  • Outer loop: enumerate(zip(atom_labels, atoms)) gives us label_i and atom_i directly

  • Inner loop: zip(atom_labels[i + 1:], atoms[i + 1:]) gives us label_j and atom_j directly

  • Inside the loop body, we just use these values - no indexing needed

Both approaches give the same result.

In the second solution we have moved the logic of handling which pairs of atoms we are working with into the for statement, i.e. specifying what we are looping over. This has allowed us to write the inner loop code without worrying about which atoms we are calculating the distance for.