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:
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 pointrange(1, n + 1)
generates the numbers 1, 2, 3, …, nEach iteration multiplies
result
by the current value ofi
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 bothatomic_masses[i]
andstoichiometric_coefficients[i]
We add the product of mass × coefficient to the running total
+=
is shorthand formolecular_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 listsThe list comprehension
[mass * coeff for mass, coeff in zip(...)]
iterates over these pairsFor each pair
(mass, coeff)
, we calculate the productmass * coeff
This creates a list of products:
[2.016, 15.999]
for watersum()
adds all the products togetherThis 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:
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-H1When
i = 2
(atom H2):j
loops over 3 (atom H3) — we already calculated N-H2 and H1-H2When
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
andj
throughout:atoms[i]
,atoms[j]
,atom_labels[i]
,atom_labels[j]
Alternative solution:
Outer loop:
enumerate(zip(atom_labels, atoms))
gives uslabel_i
andatom_i
directlyInner loop:
zip(atom_labels[i + 1:], atoms[i + 1:])
gives uslabel_j
andatom_j
directlyInside 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.