The previous lesson ended with a program that knows how to decide, but is still incapable of doing the most natural thing in the world: insisting. When Marta types superurgent where a priority should go, EasyTask reads what it can and, if it does not understand, invents a default value. The sensible thing would be to ask again. And to ask again you have to repeat.

Repeating is the second great control structure, and the one that turns a program into something that really works: walking through data, insisting until you get a valid answer, accumulating totals, working out averages. Without loops, a program can only do as many things as it has lines written; with loops, ten lines can process ten values or ten thousand.

In this lesson you will learn the two ways of repeating in Python — while and for —, when to use each one, the four patterns that come up again and again in real code, and how to finally tie up the loose end we have been carrying since module 2.

Contents

  1. Repeating without repeating code
  2. The while loop
  3. Infinite loops and how to get out of them
  4. The for loop and the range() function
  5. Walking through a string character by character
  6. while versus for: when to use each one
  7. The four classic patterns
  8. EasyTask v0.5.2: insisting until the data is valid
  9. Common mistakes and tips
  10. Exercises
  11. Conclusion

  1. Repeating without repeating code

Imagine Marta wants to see the countdown of the three days left to deliver the book fair poster. With what you knew until now, the only way out was to write three almost identical print statements. It works for three days; for thirty, that is thirty lines, and for a number the user decides while the program is running, it is simply impossible.

A loop (or iteration) solves this: you write the block once only and tell the program how many times, or until when, it should run it. Each pass through the block is called an iteration.

Python offers two loops, and the difference between them is just this: while repeats while a condition is true, without knowing in advance how many rounds it will do, and for repeats for each element of a sequence, with the number of rounds settled before it starts.

  1. The while loop

Its syntax is identical to that of the if: keyword, condition, colon and a block indented by 4 spaces. The difference is that, when the block finishes, the flow goes back to the condition instead of carrying on.

remaining_days = 3                          # 1. initialisation

while remaining_days > 0:                   # 2. continuation condition
    print(f"{remaining_days} days left")    # 3. body
    remaining_days -= 1                     # 4. update

print("Deadline reached.")

It prints 3 days left, 2 days left, 1 days left and then, already outside the loop, Deadline reached.. Every correct while loop has those four pieces, and it is worth learning them as a checklist:

Piece What it does If it is missing
Initialisation Gives an initial value to the control variable NameError: the variable does not exist
Continuation condition Evaluated before each round SyntaxError
Body The work that is repeated IndentationError
Update Changes the variable so it moves towards the exit Infinite loop

Notice an important detail: the condition is evaluated before the first round. If it is false from the outset — for example, with remaining_days = 0 — the body does not run even once. A while can do zero rounds, and that is almost never a bug: it is usually exactly what we want.

flowchart TD
    A["Initialise variable"] --> B{"Condition true ?"}
    B -->|False| E["Carry on with the program"]
    B -->|True| C["Run the body"]
    C --> D["Update variable"]
    D --> B

The arrow that goes back from the update to the condition is what distinguishes a loop from an if. It is literally a cycle.

  1. Infinite loops and how to get out of them

If you forget the update, the condition never changes and the loop never ends:

remaining_days = 3
while remaining_days > 0:
    print(f"{remaining_days} days left")     # remaining_days -= 1 is missing

The program prints 3 days left forever, until it fills the screen. It is not a syntax error: Python cannot know you have made a mistake, because an endless loop is perfectly legal (in fact, in the lesson Interactive menus we will use one on purpose).

How to stop it: press Ctrl+C in the terminal. That interrupts the program and shows a KeyboardInterrupt. It is not a fault in your code: it is the sign that you cut the execution short.

The three slips that cause them are almost always the same: not updating the control variable inside the body, updating it in the wrong direction (remaining_days += 1 when the condition is > 0), or updating it outside the loop, that is, without the right indentation, so that it only runs once the loop is over. Before you run a while, ask yourself this: which instruction in this body will eventually make the condition false? If you cannot point at it with your finger, you have an infinite loop.

  1. The for loop and the range() function

The for repeats a block for each element of a sequence. When all you want is to repeat n times, the sequence is generated by range(), which produces a run of whole numbers.

for number in range(3):
    print(f"Pass number {number}")       # prints 0, then 1, then 2

The variable number is the control variable: Python creates it, assigns it the first value of the sequence, runs the body, assigns it the next one, and so on until the sequence is exhausted. There is no need to initialise or update it: the for takes care of that, which is why a for never produces an infinite loop by accident.

range() takes three forms, and it is worth mastering all of them:

Form Generates Example Values
range(n) From 0 to n−1 range(4) 0, 1, 2, 3
range(a, b) From a up to b−1 range(1, 5) 1, 2, 3, 4
range(a, b, step) From a to b−1 jumping in steps range(0, 10, 3) 0, 3, 6, 9
range(a, b, -step) Counts down range(3, 0, -1) 3, 2, 1

The rule to burn into your memory: the upper limit is never included. range(5) gives five numbers, but 5 is not among them; it is the number-one source of off-by-one errors in beginners.

for day in range(1, 6):                  # the 5 working days
    print(f"Day {day} of the plan")

for remaining_days in range(3, 0, -1):   # countdown: 3, 2, 1
    print(f"{remaining_days} days left")
flowchart TD
    A["range generates the sequence"] --> B{"Any elements left ?"}
    B -->|False| E["Carry on with the program"]
    B -->|True| C["Assign the next one<br/>to the control variable"]
    C --> D["Run the body"]
    D --> B

Compare it with the while diagram: the structure is the same, but here there is no manual update because the sequence itself decides when it ends.

  1. Walking through a string character by character

range() is not the only sequence a for can walk through. A string is one too: its elements are its characters, from left to right.

title = "Poster"
for letter in title:
    print(letter, end=" ")      # P o s t e r

This opens the door to analysing text without knowing about data structures yet. For example, counting how many spaces a title has, which is a rough-and-ready way of counting words:

title = "Book fair poster"
spaces = 0

for character in title:
    if character == " ":
        spaces += 1

print(f"The title has {spaces + 1} words")          # 3 words

Notice the combination: a for that walks, an if that decides and a counter that accumulates. That trio is 80% of the code you will ever write. Walking through collections of tasks with for — a list holding all the studio's tasks — is exactly the same idea, and it arrives in Lists and arrays.

  1. while versus for: when to use each one

Either loop can imitate the other, but every problem has its natural shape.

while for
Used when You do not know how many rounds you will do You know the number of rounds or the sequence
Governed by A condition A sequence
Control variable You initialise and update it yourself Python manages it
Risk of an infinite loop Yes, if you forget to update Practically none
Typical example Asking for a value until it is valid Repeating a calculation 12 times

The question that almost always settles it: can I know the number of repetitions before starting? If the answer is yes, for. If it depends on something that will happen inside the loop — what the user types, a calculation that converges — then while.

  1. The four classic patterns

Almost everything done with loops falls into one of these four moulds. All four share the same anatomy: a variable that is initialised before the loop and changed inside it.

Counter: counts how many times something happens.

high_tasks = 0
for i in range(4):
    priority = input("Task priority: ").strip().lower()
    if priority == "high":
        high_tasks += 1
print(f"There are {high_tasks} high-priority tasks")

Accumulator: adds up the values it sees along the way.

total_days = 0
for i in range(3):
    total_days += int(input("Estimated days: "))
print(f"The batch adds up to {total_days} workdays")

Put the accumulator and the counter together and you get the average, which is an ordinary division: total_days / task_count. Remember from lesson 02-02 that / always returns a float.

Maximum and minimum: looks for the largest or the smallest value.

longest = 0                     # smaller than any valid estimate
for i in range(3):
    days = int(input("Estimated days: "))
    if days > longest:
        longest = days
print(f"The longest task is {longest} workdays")

The key is in the initialisation: longest = 0 works because no valid estimate is less than or equal to zero. For the minimum the symmetric trick does not work (shortest = 0 would never be beaten), so it is initialised with a deliberately high value, such as shortest = 9999, or with the first value read.

Boolean flag: remembers whether something has happened at least once.

has_urgent = False
for i in range(3):
    priority = input("Task priority: ").strip().lower()
    if priority == "high":
        has_urgent = True
if has_urgent:
    print("WARNING: the batch contains urgent items.")

The flag is initialised to False and can only turn to True; it never goes back. It is the pattern that answers questions of the kind "is there any…?".

  1. EasyTask v0.5.2: insisting until the data is valid

Now we can do it. The validation loop is the pattern we have been promising since lesson 02-04, and its shape is always the same: read the value before the loop, and repeat while it is not valid.

priority = input("Priority (high/medium/low): ").strip().lower()

while priority not in PRIORITIES:
    print(f"'{priority}' is not a valid priority.")
    priority = input("Priority (high/medium/low): ").strip().lower()

Read it slowly, because it hides a classic trap: the input() appears twice, once before the loop and once inside it. The first read is needed so that the condition has something to evaluate; the second is the update we talked about in section 2. If you forget the inner one, the value never changes and you have an infinite loop repeating the same telling-off forever. With this piece in place, EasyTask no longer invents anything: it asks until it is given a correct value. Here is the complete version 0.5.2, where the error message is built into the prompt of the second read so as not to lengthen the code.

# easytask.py - Alba Studio
# Version 0.5.2: insistent input and batch summary

WIDTH = 46
HOURS_PER_WORKDAY = 8
PRIORITIES = ("high", "medium", "low")
TEAM = ("Marta", "Luis", "Nuria")
DAYS_MIN = 1
DAYS_MAX = 365

# --- 1. Title: insist while it is empty ---
title = input("Task title                 : ").strip()
while title == "":
    title = input("Empty. Task title          : ").strip()

# --- 2. Assignee: insist while not on the team ---
assignee = input("Assignee (Marta/Luis/Nuria): ").strip().capitalize()
while assignee not in TEAM:
    assignee = input("Not on the team. Assignee  : ").strip().capitalize()

# --- 3. Priority: insist while not in the catalogue ---
priority = input("Priority (high/medium/low) : ").strip().lower()
while priority not in PRIORITIES:
    priority = input("Not valid. Priority        : ").strip().lower()

# --- 4. Days: insist while not a number within range ---
days_text = input("Estimated days             : ").strip()
while not days_text.isdigit() or not DAYS_MIN <= int(days_text) <= DAYS_MAX:
    days_text = input(f"Integer {DAYS_MIN} to {DAYS_MAX}. Days   : ").strip()
estimated_days = int(days_text)

# --- Task card ---
estimated_hours = estimated_days * HOURS_PER_WORKDAY
print("=" * WIDTH)
print(f"{'Title':<16}{title:>{WIDTH - 16}}")
print(f"{'Assignee':<16}{f'{assignee} ({priority})':>{WIDTH - 16}}")
print(f"{'Days / hours':<16}{f'{estimated_days} / {estimated_hours}':>{WIDTH - 16}}")
print("=" * WIDTH)

Look at the condition of the fourth loop, which brings together everything in the module: not days_text.isdigit() or not DAYS_MIN <= int(days_text) <= DAYS_MAX. Thanks to the short-circuiting of or, if the text is not a number the second part is not evaluated and int() never gets to run on rubbish. A single condition validates type and range at the same time.

None of the four inputs can now lead to a made-up value. Compare it with version 0.5.1: there the program corrected; here it demands. A snippet of a session:

Priority (high/medium/low) : superurgent
Not valid. Priority        : HIGH
Estimated days             : four
Integer 1 to 365. Days     : 4

The batch summary

Marta usually plans several tasks at once: she knows how many there are and wants to know how many workdays they add up to and what the average is. Since we know the number of rounds before starting, this is for territory, with three of the patterns from section 7 all at once.

# --- Batch planning summary ---
task_count = int(input("How many tasks are you going to plan? "))
total_workdays = 0
longest = 0

for i in range(task_count):
    days = int(input(f"Workdays for task {i + 1}: "))
    total_workdays += days
    if days > longest:
        longest = days

average = total_workdays / task_count
print(f"Batch total     : {total_workdays} workdays")
print(f"Average per task: {average:.1f} workdays")
print(f"Longest one     : {longest} workdays")

With workdays of 4, 10 and 1, the summary comes out as Batch total: 15 workdays, Average per task: 5.0 workdays and Longest one: 10 workdays.

Three details deserve attention. The i + 1 in the message exists because range() starts at 0 and people count from 1. The :.1f is the f-string specifier from lesson 02-03, needed because / returns a float with too many decimal places. And the most important one: each task's data is lost the moment the next one is read. We can add it up and compare it on the fly, but not store it: EasyTask still only has room for a single task. To hold several we need lists.

Common Mistakes and Tips

Forgetting the update in a while. The classic mistake and the one that produces infinite loops. Before running, point your finger at the line that will make the condition false.

Forgetting the second input() in a validation loop. If you only read before the while, the value never changes and the program repeats the same warning for ever. Ctrl+C and go and check the indentation.

Miscounting with range(). range(5) gives 0, 1, 2, 3, 4: the upper limit is not included. If you want 1 to 5, write range(1, 6).

Initialising the accumulator inside the loop. Putting total = 0 inside the body resets it on every round, and at the end it holds the same as the last value read: the initialisation always goes before the loop. And watch out for the control variable of a for: after for i in range(3):, i is 2, not 3, and changing it inside the body does not affect the following rounds.

Tip: write the exit condition first. Before typing the body, ask yourself when the loop should end. A loop with no clear idea of its ending is a potential infinite loop.

Tip: test your loops with zero rounds. What happens if the user says they are going to plan 0 tasks? The for does no rounds at all and the average causes a division by zero. Edge cases (0 rounds, 1 round) uncover most of the bugs.

Exercises

Exercise 1: Predict the output

Without running anything, say what each fragment prints and how many rounds it does.

# a)
for i in range(2, 8, 2):
    print(i, end=" ")
# b)
n = 5
while n > 5:
    n -= 1
print("end", n)
# c)
total = 0
for i in range(1, 5):
    total += i
print(total)

Exercise 2: Vowel counter for a title

Write count_vowels.py which asks for a task title and, walking through it character by character with a for, counts how many vowels it has (normalise to lower case first with .lower() and use in "aeiou"). Show the total and the percentage of vowels over the length of the title, to one decimal place. Hint: len(title) returns the number of characters.

Exercise 3: Sharing out the team's workdays

Write team_allocation.py which asks Marta how many tasks she wants to share out and, for each one, asks for the estimated workdays (validating with a while that it is an integer between 1 and 30) and the assignee (validating with another while that they are in TEAM). When it finishes it must show the total workdays for the batch, the average per task and whether there is any task of more than 15 workdays, using a boolean flag.

Solutions

Solution 1.

Output Rounds
a 2 4 6 3 — range(2, 8, 2) generates 2, 4 and 6; 8 is left out
b end 5 0 — the condition 5 > 5 is false from the outset, so the body never runs
c 10 4 — it accumulates 1+2+3+4; 5 is not in range(1, 5)

Case b is the most surprising one: a while can do zero rounds. And c is a reminder that the upper limit of range() is always excluded.

Solution 2.

# count_vowels.py - Alba Studio
title = input("Task title: ").strip()
while title == "":
    title = input("The title cannot be empty. Title: ").strip()

vowels = 0
for character in title.lower():
    if character in "aeiou":
        vowels += 1

percentage = vowels / len(title) * 100
print(f"'{title}' has {len(title)} characters and {vowels} vowels ({percentage:.1f}%)")

Two notes. The .lower() is applied to the string the for walks through, not to each character: normalising once is cheaper and clearer than doing it on every round. And character in "aeiou" uses the in operator from lesson 02-02, which on strings checks whether one piece of text is contained in another; with a single character it amounts to asking whether it is one of those five letters.

Solution 3.

# team_allocation.py - Alba Studio
TEAM = ("Marta", "Luis", "Nuria")
task_count = int(input("How many tasks are you sharing out? "))
total_workdays = 0
has_long_task = False

for i in range(task_count):
    print(f"--- Task {i + 1} ---")
    days_text = input("Estimated workdays (1-30): ").strip()
    while not days_text.isdigit() or not 1 <= int(days_text) <= 30:
        days_text = input("Must be an integer from 1 to 30. Workdays: ").strip()
    assignee = input("Assignee (Marta/Luis/Nuria): ").strip().capitalize()
    while assignee not in TEAM:
        assignee = input("Not on the team. Assignee: ").strip().capitalize()
    total_workdays += int(days_text)
    if int(days_text) > 15:
        has_long_task = True

average = total_workdays / task_count
print(f"Total: {total_workdays} workdays | Average: {average:.1f} per task")
if has_long_task:
    print("WARNING: there are tasks of more than 15 workdays. Worth splitting them.")

Look at the structure: a for that knows how many rounds it will do contains two while loops that do not. That combination — for for the countable, while for the insistent — is the one that shows up in real code. And the has_long_task flag is declared before the for, because it has to survive every round; if it were inside, it would reset on each task and at the end would only reflect the last one.

Conclusion

You now know how to repeat. You know the while loop with its four pieces — initialisation, continuation condition, body and update —, you know that the condition is evaluated before each round (and that this is why a while can do zero rounds), and you know what an infinite loop is, why it happens and that you get out of it with Ctrl+C. You know the for loop with the three forms of range() — remembering that the upper limit is never included — and its ability to walk through a string character by character. And you have the criterion for choosing: if you can count the rounds before starting, for; if they depend on what happens inside, while.

You also have the four patterns that solve most loop problems: counter, accumulator, maximum/minimum and boolean flag, all with the same anatomy of a variable initialised outside and changed inside. And EasyTask, in its version 0.5.2, has finally tied up the loose end we had been dragging along since module 2: it no longer corrects or invents data, it insists until Marta types something valid, with one validation loop per field; on top of that it can summarise a batch of tasks — total, average and maximum — even though it cannot store them.

Loose ends remain, and they are the ones that open the next lesson. If Marta gets it wrong five times in a row, the program interrogates her five times without mercy: there is no way to give up after a reasonable number of attempts, nor to skip one particular round, nor to cut a loop off halfway through the body when the answer is already known. Nor do we yet know how to combine two loops, one inside the other, to walk through a grid of days per person. All of that is fine-grained flow control, and it is what you will learn in Advanced control structures: break, continue, the surprising else clause of loops, nested loops and the match statement for classifying values more elegantly than a chain of elif.

© Copyright 2026. All rights reserved