Repeating work

65 min

Listen: this lesson as a conversation

Two hosts talk the lesson through. The voices are synthetic; the script was written from this lesson and checked against it, and asserts nothing the lesson does not.

In this lesson you will learn to
  • Compare for and while, and explain which one a given job needs
  • Build an accumulator loop that totals and counts, and explain why it starts outside the loop
  • Predict the output of a loop over range(), including one with a step
  • Identify an off-by-one error and an unguarded empty case in someone else's loop

By the end of lesson 3 your programs could store things and choose between them. They still can't do the one thing computers are actually for: the same work, over and over, without getting bored or making a mistake on the four hundredth try.

Going through things you already have

readings = [12, 7, 19, 4]

for r in readings:
    print(r)

for takes each item in turn, puts it in the name r, and runs the indented block. Four items, four passes, then it stops on its own. You don't count anything and you can't run off the end.

You'll also meet range(), which produces a run of numbers without you writing them out. Everything printed in this lesson was run on CPython 3.14.7 and pasted in as it came out, with only the file paths changed.5

>>> list(range(5))
[0, 1, 2, 3, 4]
>>> list(range(1, 10))
[1, 2, 3, 4, 5, 6, 7, 8, 9]

Look at the second one. You asked for 1 to 10 and got 1 to 9. The stop value is never included, and that catches everyone once.1

It isn't an arbitrary cruelty. Because the stop is excluded, range(n) has exactly n items in it, and range(0, 5) followed by range(5, 10) covers 0 to 9 with nothing missed and nothing repeated, which is a good share of the off-by-ones you would otherwise write.

Predict first

range() takes a third number, the step. What does for i in range(2, 11, 3): print, if the body is print(i, end=" ")?

Show the answer

2 5 8.

It starts at 2 and adds 3 each time. The next would be 11, but the stop is 11 and the stop is never reached, so it finishes. Ask for range(2, 12, 3) and you'd get 2 5 8 11.

The accumulator

Almost every loop that produces an answer looks like this. Set something up before the loop, change it inside, use it after.3

readings = [12, 7, 19, 4]
total = 0
count = 0

for r in readings:
    total = total + r
    count = count + 1

print("Average:", total / count)

At the end of each pass the three names stand like this:

Pass r total count
1 12 12 1
2 7 19 2
3 19 38 3
4 4 42 4

Then 42 / 4, which is 10.5.

total = total + r is the line from lesson 2 that looked like nonsense as arithmetic and reads fine as an instruction: work out the right side using what total is worth now, then make total refer to the answer. It's doing that four times, each time starting from where the last pass left it.

Which is exactly why total = 0 has to sit outside the loop. Move it inside and it runs on every pass:

for r in readings:
    total = 0
    total = total + r

Now every pass wipes the total and adds one number to zero, so what survives at the end is 4, the last reading. No error, no warning, just a wrong answer that happens to look plausible.

If a total ever comes out equal to the last item in your data, one of two slips produced it: the reset is inside the loop, or you wrote total = r where you meant total = total + r. Both throw away everything before the final pass, and both give exactly the same symptom, so check for both.

Waiting for something instead

for needs something to go through. Sometimes you don't have one yet, because you're waiting for a condition to change:

readings = []

while True:
    line = input("Reading, or 'done': ")
    if line == "done":
        break
    readings.append(float(line))

print(readings)

while takes a condition and repeats as long as it's true. while True: is a loop that would never stop by itself, so something inside has to end it, and break does: it leaves the loop immediately. .append() puts a value on the end of a list, which is lesson 6's subject arriving early because we need somewhere to put things.

The rule of thumb: for when you know what you're going through, while when you're waiting for something to become true. If you can't say what you'd be iterating over, you want while.

Check yourself

The condition of a while is tested for truth, exactly like an if. Given what lesson 3 said about empty things, what does while readings: do, and when does it stop?

Show the answer

It repeats as long as readings has anything in it, and stops when the list is empty, because an empty list is false. You'd use it for a loop that consumes the list as it goes. Note it's while readings: and not while len(readings) > 0:, which says the same thing in more words.

The two ways this goes wrong

The loop that never ends. Run this one on purpose:

n = 5
while n > 0:
    print(n)
5
5
5
5
... and so on for ever

Nothing inside the loop changes n, so n > 0 is true now and will be true for ever. Press Ctrl-C to stop it.2 Python reports the interruption like this:

  File "/home/you/spin.py", line 3, in <module>
    print(n)
    ~~~~~^^^
KeyboardInterrupt

The fix is one line, n = n - 1 at the bottom of the body. That's the cause almost every time: something inside was supposed to change what the condition tests, and doesn't.

The empty case. Put the two halves together, the while that collects and the for that averages, and save the whole thing as readings.py:

readings = []

while True:
    line = input("Reading, or 'done': ")
    if line == "done":
        break
    readings.append(float(line))

total = 0
count = 0
for r in readings:
    total = total + r
    count = count + 1

print("Average:", total / count)

Type 12, 7, 19 and then done and it says Average: 12.666666666666666. Now run it again and type done straight away:

Reading, or 'done': done
Traceback (most recent call last):
  File "/home/you/readings.py", line 15, in <module>
    print("Average:", total / count)
                      ~~~~~~^~~~~~~
ZeroDivisionError: division by zero

Read it the way lesson 1 taught, from the bottom. ZeroDivisionError: division by zero names the fault. Above it, line 15 and the source line that failed, and under that the marker ~~~~~~^~~~~~~, which points not at the whole line but at total / count, the exact part that blew up. Those markers arrived in Python 3.11 and they are the most useful thing in a traceback once a line has more than one operation in it.4

So: the loop body never ran, total and count both still hold the 0 they were given, and the last line divides zero by zero. That's the fifth error in this course, and the empty case is the one people forget, because they test by typing three readings in like a reasonable person. A real user opens it, doesn't understand it, and presses Enter.

The fix is an if before you divide, which is why lesson 3 came first:

if count > 0:
    print("Average:", total / count)
else:
    print("No readings.")

One more, smaller than the other two but far more common. This loop is meant to print every item in the list. It prints three of the four.

items = ["a", "b", "c", "d"]

for i in range(1, len(items)):
    print(items[i])
Check yourself

Which of the four items goes missing, and why?

Show the answer

"a" is missed, and the output is b c d.

A list's first item is at position 0, not 1, so starting the range at 1 skips it. len(items) is 4, so range(1, 4) gives 1, 2, 3. The fix is range(len(items)), which gives 0, 1, 2, 3, and the better fix is for item in items:, which needs no numbers at all and cannot be off by one. Positions come up properly in lesson 6.

What people get wrong

Expecting range(1, 10) to include 10. It stops before the stop, and saying that sentence out loud once is what makes it stick.

Resetting the accumulator inside the loop. Covered above, and the tell is a total equal to the last item.

Thinking for and while are interchangeable. Anything a for does you can force a while to do by counting by hand, and you'll introduce an off-by-one doing it, so pick by the shape of the problem instead.

Changing a list while looping over it. Adding to or removing from a list inside a for that's walking it produces skipped items and other confusion. Lesson 6 has lists properly and shows this; for now, just don't.

Practice

Two loops

Take 25 minutes over these.

One. Two loops over the same data. Given a list of numbers, print how many of them are above the list's own average. This needs two loops rather than one, and working out why is most of the exercise: nothing can be compared with the average until the last number has been seen. Check yours against [5, 5, 5, 40], where the answer is 1, and be sure you can say why it isn't 3.

Two. Build up a string. An accumulator doesn't have to hold a number. Start with word = "" and loop over the letters of "loop", and on each pass set word to the letter followed by whatever word already held. Predict what comes out before you run it. Then work out what changes if you write it the other way round.

Three. Four plans in one program. This is the one that matters, and it's harder than it looks. Write a program that reads numbers one per line until the user types done, ignores any number below zero and says so when it does, and then reports how many valid readings there were and their average, without crashing if there weren't any.

Every piece is something you've already done: terminate on a sentinel, guard with an if, accumulate a total, count. Putting four easy pieces into one program is a separate skill from having the pieces, and it's the thing beginners reliably find hard, so build it in stages rather than all at once. Get it reading and echoing one number first. Then the sentinel. Then the rejection. Then the totals. Then the empty case.

This is the course project in miniature, and you'll meet it again at full size.

If a loop does something you can't explain, step through it in Python Tutor, which shows every name changing on every pass. Here is the averaging loop above, already loaded: press Next repeatedly and watch total and count climb while r takes each reading in turn.

Connections

You've now written total = total + r and count = count + 1 several times, and you'd write them again for the next set of readings, and again after that. Copying three lines around a program is how a small mistake gets into four places at once and is fixed in three of them.

That's the problem the next lesson solves. A function lets you write the averaging once, give it a name, and call it wherever you need it. The order here is deliberate: you've felt the repetition before being handed the tool that removes it, because a tool you've wanted is much easier to learn than a tool you've been given.

Go deeper

Sources

  1. The Python Tutorial, chapter 4, "More Control Flow Tools", Python 3.14 documentation. for, range, break, and while, and the rule that a range's stop value is excluded.
  2. Al Sweigart, Automate the Boring Stuff with Python, 3rd edition, chapter 3, "Loops". Free online under CC BY-NC-SA 3.0, and linked rather than adapted. The source for treating the runaway loop and Ctrl-C as a topic a beginner meets early rather than late.
  3. Allen B. Downey, Think Python, 3rd edition 2023, chapter 7, "Iteration and Search". Free online under CC BY-NC-SA 4.0, linked rather than adapted. Downey builds the accumulator pattern the same way, setting up before the loop and using after it.
  4. PEP 657, "Include Fine Grained Error Locations in Tracebacks", Python 3.11. The ~~~~~~^~~~~~~ markers under the failing part of a line.
  5. All code output in this lesson was run on CPython 3.14.7 and pasted from the terminal, including the range() results, the pass-by-pass table, both tracebacks with their real line numbers, and the reset-inside-the-loop total of 4. Paths in the tracebacks are shown as /home/you/ in place of the machine's own.

Check your understanding

This lesson has a 5-question quiz. Pass it and the questions come back on a schedule in Review, so what you learned stays learned. Your progress is saved in your browser; no account needed.