Repeating Work: Loops and a Step Counter
for, range, and while — plus counting how many steps your program really takes.
- make decisions with
if/elif/else; - repeat work with a
forloop and awhileloop; - stop or skip a loop early with
breakandcontinue; - add a counter to any program to measure the work it does;
- predict the step count of a loop before running it;
- recognise why a loop inside a loop is a different animal entirely.
3.1Decisions first
Before repetition, one small tool: doing something only in certain cases. Python checks the condition, and runs the indented block only if it is true.
temperature = 31
if temperature > 30:
print("Hot")
elif temperature > 15:
print("Pleasant")
else:
print("Cold")
Two things trip up beginners: the colon at the end of the line, and the
indentation (four spaces). In Python the indentation is not
decoration — it is how the language knows which lines belong to the if.
| Comparison | Means |
|---|---|
== | is equal to (two signs — one sign is assignment!) |
!= | is not equal to |
> < >= <= | greater / less / at least / at most |
and or not | combine conditions |
The order matters. Python checks the branches top to bottom and takes the
first one that is true, then skips the rest. That is why the "Pleasant" branch
can safely say just > 15: if the temperature had been above 30 we would
already have stopped at "Hot". Read a chain of elifs as "otherwise, if…".
3.2The for loop: do this n times
for i in range(5):
print("step", i)
range(5) produces 0, 1, 2, 3, 4 — five values, starting at zero, stopping
before five. Programmers count from zero often enough that you will stop
noticing within a fortnight.
range(5)→ 0 1 2 3 4range(1, 6)→ 1 2 3 4 5range(0, 10, 2)→ 0 2 4 6 8 (step of two)
Adding up the numbers from 1 to n is the classic first loop:
n = 100
total = 0
for number in range(1, n + 1):
total = total + number
print(f"The numbers 1 to {n} add up to {total}")The pattern — start an accumulator at zero, add to it inside the loop, use it after — is worth memorising. You will write it a hundred times.
3.3The while loop: keep going until
Use for when you know how many repetitions you need, while
when you do not.
remaining = 1000
looks = 0
while remaining > 1:
remaining = remaining // 2 # throw away half the dictionary
looks = looks + 1
print(f"Pages left: {remaining}, looks needed: {looks}")That is the dictionary trick from week 1, now measured by the computer instead of by hand. Change 1000 to 1 000 000 and run it again: the answer goes to 20, not to 10 000.
If the condition never becomes false, the cell runs forever. In Colab, press the stop button (■) next to the cell. Every programmer does this in their first week; it breaks nothing. The usual cause: you forgot to change, inside the loop, the very thing the condition tests.
3.4Counting the work
Here is the technique that carries the whole course. Add a variable whose only job is to count how many times the interesting line runs.
n = 50
steps = 0
total = 0
for number in range(1, n + 1):
total = total + number
steps = steps + 1 # one unit of work
print(f"n = {n}, result = {total}, steps = {steps}")
Run it with n = 50, then 100, then 200. The step count is always exactly
n. Double the input, double the work: a straight-line relationship. In
week 8 this gets the name O(n), but you can already see it in your own output.
For a loop this small, timing is useless — the numbers bounce around depending on what else your machine is doing. Counting is exact and repeatable. We will add the stopwatch in week 5 and use both together for the rest of the course.
3.5A loop inside a loop
Now the moment that makes people sit up. Put one loop inside another:
n = 5
steps = 0
for i in range(n):
for j in range(n):
steps = steps + 1
print(f"n = {n}, steps = {steps}")
The inner loop runs n times for each pass of the outer loop, so
the total is n × n. Fill in this table by running the code — do not guess:
| n | single loop steps | nested loop steps |
|---|---|---|
| 5 | 5 | 25 |
| 10 | 10 | 100 |
| 100 | 100 | 10 000 |
| 1 000 | 1 000 | 1 000 000 |
| 10 000 | 10 000 | 100 000 000 |
Ten times more input, a hundred times more work. This is why "it worked fine on my test data" and "it froze on the real data" are the same program. Nested loops are not forbidden — sometimes you genuinely must compare every pair — but from now on, when you write one, you should feel a small alarm go off.
3.6Deciding inside a loop: break and continue
Combining a decision with a loop is where loops earn their keep. Two keywords give you
fine control: break leaves the loop immediately, and continue
skips the rest of the current pass and moves on to the next.
Early exit with break. Suppose we want the first number
whose square is over 1000, and we want to know how many we had to check:
steps = 0
answer = None
for k in range(1, 1000):
steps = steps + 1
if k * k > 1000:
answer = k
break # found it — no need to look further
print(f"first is {answer}, checked {steps} numbers")
Without break the loop would grind on to 999 for no reason. Stopping early
is exactly what makes a "best case" cheaper than a "worst case" — a distinction we make
precise when we search lists in week 4.
Skipping with continue. Add up only the numbers from 1 to
20 that are not multiples of 3:
total = 0
skipped = 0
for number in range(1, 21):
if number % 3 == 0:
skipped = skipped + 1
continue # jump straight to the next number
total = total + number
print(f"total {total}, skipped {skipped}")
Notice that continue did not save any work in the big-picture sense — the
loop still visited all 20 numbers, so the step count is still n. It changed
what happens on a pass, not how many passes there are. Keep that
separation clear: skipping an item is not the same as never visiting it.
3.7Common mistakes with loops
Four loop bugs account for most of the confusion in these first weeks. Meet them here so you recognise them in your own cells tonight.
| Mistake | Symptom | Fix |
|---|---|---|
Off-by-one in range | Loop stops one short, or runs one too many | Remember range(a, b) stops before b; for 1…n use range(1, n + 1). |
| Resetting inside the loop | Your total or counter is always tiny or wrong | Set the accumulator to 0 above the loop, not inside it. |
| Wrong indentation | A line runs once after the loop instead of every pass (or vice versa) | Lines inside the loop are indented under it; lines after it are not. |
Infinite while | The cell never finishes | Make sure the loop body changes the variable the condition tests. |
The second one is subtle enough to show. Both loops below look reasonable; only one gives the right total for 1 to 5, which is 15:
# WRONG: total is wiped clean on every pass
for number in range(1, 6):
total = 0
total = total + number
print("wrong version:", total) # prints 5, not 15
# RIGHT: total starts at 0 once, then grows
total = 0
for number in range(1, 6):
total = total + number
print("right version:", total) # prints 15In the wrong version, every pass throws the running total away and rebuilds it from zero, so at the end it only holds the last number. The lesson is the one-line habit: initialise accumulators above the loop. When a total or a counter comes out wrong, this is the first place to look.
When a loop confuses you, trace it for a tiny input — say n = 3 — writing
down the value of every variable after each pass. Three rows of a table almost always
reveal the bug, and it is far faster than staring at the whole thing.
3.8Try it yourself
Print 10, 9, 8 … 1, then "Lift off". Use a loop, not ten print lines.
Solution
for i in range(10, 0, -1):
print(i)
print("Lift off")Ten steps for a countdown from 10, a hundred from 100: the work is proportional to where you start.
Count how many numbers between 1 and n are even, and also count how
many steps your program takes. Try n = 100, 200, 400 and note
both numbers.
Solution
n = 100
evens = 0
steps = 0
for number in range(1, n + 1):
steps = steps + 1
if number % 2 == 0:
evens = evens + 1
print(f"n = {n}: {evens} evens, {steps} steps")Steps: 100, 200, 400 — exactly n. Answer: 50, 100, 200. Both grow in a straight line, and doubling n doubles both.
Write down, on paper, the step count you expect from the nested loop for
n = 300. Then run it. Then explain any surprise in one sentence.
Answer
90 000. If you guessed 600 you added when you should have multiplied — a nested loop multiplies, and that single mistake is behind most "why is my program frozen?" questions.
Modify the while loop from §3.3 so the starting size is a variable. Record the "looks" for 1 000, 10 000, 100 000, 1 000 000. What pattern do you see?
What you should see
10, 14, 17, 20. Multiplying the input by ten adds only three or four steps. Compare that with the nested-loop table above — these two behaviours are as different as walking and teleporting.
Using break, find the first number from 1 upward that is a multiple of
both 7 and 9, and report how many numbers you had to check. Predict the count before
you run it.
Solution
steps = 0
answer = None
for k in range(1, 1000):
steps = steps + 1
if k % 7 == 0 and k % 9 == 0:
answer = k
break
print(f"answer {answer}, checked {steps}")
63 is 7 × 9, the first number both divide, so the loop stops after checking 63.
Without break it would have run all the way to 999.
For numbers 1 to 20, print "Fizz" if the number is a multiple of 3, "Buzz" if a multiple of 5, "FizzBuzz" if both, and the number itself otherwise.
Solution
for n in range(1, 21):
if n % 3 == 0 and n % 5 == 0:
print("FizzBuzz")
elif n % 3 == 0:
print("Fizz")
elif n % 5 == 0:
print("Buzz")
else:
print(n)The order of the branches is the whole trick: test "both" first, because a multiple of 15 is also a multiple of 3, and Python takes the first true branch. Put the "Fizz" test first and 15 would wrongly print "Fizz".
3.9Self-check
How many numbers does range(1, 11) produce?
A single loop over n items takes 1 000 steps when n = 1 000. A nested double loop over the same data takes:
You double n from 500 to 1 000. Which program's step count grows the least?
What does break do inside a loop?
break exits the loop then and there. The skip-this-pass keyword is continue.A loop that should total 1 to 5 keeps printing 5. The most likely cause is:
3.10Homework
- Create
AA_Week03.ipynband solve Tasks 1–6. - Write a program that prints the multiplication table from 1×1 to n×n using nested loops, with a step counter. Run it for n = 5, 10, 20 and record the counts.
- Write a program that finds the first number above 1 that is divisible by 2, 3, 4, 5 and 6, using
break, and reports how many numbers it checked. Explain the result in one sentence. - Make a table in a text cell: n, single-loop steps, nested-loop steps, halving-loop steps, for n = 10, 100, 1 000, 10 000.
- Write five sentences on your table: which column worries you most as n grows, and why? Which would you be happy to run on a million rows?
3.11Words from this week
| Term | Meaning in plain words |
|---|---|
| loop / iteration | Repeating a block of instructions; one pass through it. |
range(a, b) | The numbers from a up to but not including b. |
| accumulator | A variable that collects a running total or count. |
| step counter | An accumulator whose only job is to measure the work done. |
break / continue | Leave the loop now / skip to the next pass. |
| nested loop | A loop inside a loop — the counts multiply, not add. |
| indentation | The four spaces that tell Python which lines belong inside a block. |
3.12Chapter problem set — Skiena 2.10
These are exercises from Skiena's Algorithm Design Manual, Chapter 2, matched to this week and solved step by step. They are all about the same skill you practised above: counting how many times a loop runs, and turning that count into a tidy formula.
Consider this fragment. The outer loop runs i from 1 to n; for
each i the inner loop runs j from i up to
2*i, printing "foobar" each time. Let T(n) be the total number
of "foobar"s printed. (a) Write T(n) as a summation.
(b) Simplify it to a closed form.
for i in range(1, n + 1): # i = 1, 2, ..., n
for j in range(i, 2 * i + 1): # j = i, i+1, ..., 2i
print("foobar")Worked solution
Start with one pass of the outer loop. Fix a value of i.
The inner loop runs j from i up to and including 2*i.
How many whole numbers are there from i to 2*i? Count them:
it is (2i − i) + 1 = i + 1. The "+1" is the same off-by-one care from
§3.2 — both ends are included, so you add one. Quick check with i = 3:
j takes 3, 4, 5, 6 — that is four values, and i + 1 = 4. Good.
(a) Add up every pass of the outer loop. The outer loop does this for
i = 1, 2, …, n, so we add i + 1 for each of those:
T(n) = ∑i=1n (i + 1)
(b) Simplify. Split the sum into two easier sums:
T(n) = (1 + 2 + … + n) + (1 + 1 + … + 1)
The first bracket is the famous "add 1 up to n" from §3.2, which equals
n(n+1)/2. The second bracket is just the number 1 added n
times, which is n. So:
T(n) = n(n+1)/2 + n = (n² + n + 2n)/2 = (n² + 3n)/2
The biggest piece is the n², so for large n this grows like
a nested loop should — it is Θ(n²), the two-loops-multiply behaviour from §3.5.
A step counter confirms the formula for small n:
for n in range(1, 6):
steps = 0
for i in range(1, n + 1):
for j in range(i, 2 * i + 1):
steps = steps + 1 # one "foobar"
formula = (n * n + 3 * n) // 2
print(f"n = {n}: counted {steps}, formula {formula}")Answer: (a) T(n) = ∑i=1n (i + 1);
(b) T(n) = n(n+1)/2 + n = (n² + 3n)/2, which is Θ(n²).
Show that the alternating sum
1² − 2² + 3² − 4² + … + (−1)k−1 k²
equals (−1)k−1 · k(k+1)/2. (We will convince
ourselves it is true rather than write a formal proof.)
Worked solution
What the statement says. On the left we add the squares
1, 4, 9, 16, … but flip the sign on every second one: plus, minus, plus, minus.
The claim is that this messy-looking total is always just k(k+1)/2 —
the plain "add 1 up to k" number — carrying a plus sign when k is odd and
a minus sign when k is even (that is all (−1)k−1
does: it is +1 for odd k, −1 for even k).
Check the pattern for the first few k. The quickest way to trust an identity is to compute both sides on small cases and watch them line up:
| k | Left side (the alternating sum) | Right side (−1)k−1·k(k+1)/2 |
|---|---|---|
| 1 | 1 = 1 | + 1·2/2 = 1 |
| 2 | 1 − 4 = −3 | − 2·3/2 = −3 |
| 3 | 1 − 4 + 9 = 6 | + 3·4/2 = 6 |
| 4 | 1 − 4 + 9 − 16 = −10 | − 4·5/2 = −10 |
| 5 | … + 25 = 15 | + 5·6/2 = 15 |
Why it works — pair the terms. Group each minus with the plus just
before it: (1² − 2²), then (3² − 4²),
and so on. Each pair is a difference of squares, and there is a neat fact hiding in it:
(2m−1)² − (2m)² = −(4m − 1) = −(2m−1) − (2m)
In plain words, each plus-minus pair collapses to just the negative sum of the two
numbers involved. So after pairing, the whole alternating sum of squares turns into
a plain running total of 1, 2, 3, … (with a sign) — and a plain running total of
1 up to k is exactly k(k+1)/2. The sign at the end is whatever
the last term carried: plus for odd k, minus for even k. That
is the right-hand side. (If k is odd there is one unpaired plus term left
over at the end, and the arithmetic still lands on the same formula — worth checking
yourself as an optional challenge.)
A four-line check confirms both sides agree all the way to k = 20:
for k in range(1, 21):
left = sum((-1) ** (i - 1) * i * i for i in range(1, k + 1))
right = (-1) ** (k - 1) * k * (k + 1) // 2
print(k, left, right, left == right)Answer: the identity holds:
1² − 2² + … + (−1)k−1 k² = (−1)k−1 · k(k+1)/2,
verified for k = 1…20 and explained by pairing consecutive terms.
In the song "The Twelve Days of Christmas", on the first day my true love sends 1 gift,
on the second day 2 new gifts plus the 1 from before, and so on — each day you
receive that day's number of gifts on top of everything from the earlier days. If
Christmas lasts n days, exactly how many presents arrive in total?
Worked solution
How many arrive on a single day. On day d the true love
delivers 1 + 2 + … + d presents (the first gift, plus the second,
…, up to the d-th). That is the "add 1 up to d" number again, so a
single day d brings d(d+1)/2 presents.
Add up all the days. The grand total over n days is the
sum of each day's haul:
Total(n) = ∑d=1n d(d+1)/2
A small table makes the running total concrete:
| Day n | Presents that day = n(n+1)/2 | Total so far |
|---|---|---|
| 1 | 1 | 1 |
| 2 | 3 | 4 |
| 3 | 6 | 10 |
| 4 | 10 | 20 |
The closed form. Those running totals 1, 4, 10, 20, … are the tetrahedral numbers (imagine stacking triangular layers of cannonballs into a pyramid). The tidy formula for the sum above is:
Total(n) = n(n+1)(n+2)/6
Check it against the table: n = 4 gives 4·5·6/6 = 20.
For the real song, n = 12: 12·13·14/6 = 2184/6 = 364 —
one present for (almost) every day of the year. A tiny program agrees:
for n in (1, 2, 3, 4, 12):
total = 0
for d in range(1, n + 1):
total = total + d * (d + 1) // 2 # presents delivered on day d
formula = n * (n + 1) * (n + 2) // 6
print(f"n = {n}: total {total}, formula {formula}")
Notice the highest power is n³, so the present count grows like
Θ(n³) — even faster than the nested-loop Θ(n²) tables of §3.5.
Answer: Total(n) = ∑d=1n d(d+1)/2 = n(n+1)(n+2)/6,
which for n = 12 is 364 presents. The growth is Θ(n³).
You can repeat work and count the repetitions. Week 4 gives you something worth repeating over — lists — and you will watch the count grow with the data.