The loops in the previous lesson have one virtue and one flaw, and they are the same thing: they are all or nothing. A while insists until the condition turns false, whatever it takes; a for walks through its entire sequence even if it already knows the answer on the first round. If Marta gets the priority wrong five times in a row, EasyTask interrogates her five times without the slightest mercy: it does not know how to give up.

This lesson adds fine-grained flow control: instructions that cut a loop short halfway, that skip one particular round, that tell the difference between "I finished searching and found it" and "I finished searching and it was not there", and that combine two loops one inside the other. We close with match, the statement Python added in version 3.10 to classify values more elegantly than a chain of elif. They are powerful tools and, for that very reason, easy to misuse: a break dropped in without judgement turns a readable loop into a maze, so we will also learn when not to use them.

Contents

  1. break: leaving the loop early
  2. continue: skipping a round
  3. The else clause of loops
  4. Nested loops
  5. Sentinels and flags for leaving nested loops
  6. match / case: classifying elegantly
  7. Good practice in flow control
  8. EasyTask v0.5.3: three attempts and classification with match
  9. Common mistakes and tips
  10. Exercises
  11. Conclusion

  1. break: leaving the loop early

break interrupts the loop immediately. It does not finish the round in progress or evaluate the condition again: it abandons the loop and the program carries on at the first instruction after it.

for i in range(5):
    answer = input(f"Task {i + 1} (empty Enter to finish): ").strip()
    if answer == "":
        print("Registration interrupted by the user.")
        break
    print(f"Registered: {answer}")

print("End of registration.")

If the user presses Enter on the third task, the fourth and fifth rounds do not run. The loop was set up for five iterations and has done three.

flowchart TD
    A["Start of the round"] --> B{"break condition ?"}
    B -->|True| F["Leave the loop"]
    B -->|False| C{"continue condition ?"}
    C -->|True| A
    C -->|False| D["Rest of the body"]
    D --> A
    F --> G["Instruction after the loop"]

The most frequent use of break is the controlled infinite loop: while True: goes round forever and the only way out is a break inside the body. It sounds reckless, but it is a perfectly respectable pattern when the exit condition can only be evaluated halfway through the round, that is, after a value has been read.

while True:
    days_text = input("Estimated days (0 to cancel): ").strip()
    if days_text == "0":
        break
    if days_text.isdigit():
        print(f"{days_text} workdays noted down.")

The main loop of an application is built on this skeleton, which is exactly the subject of Interactive menus.

  1. continue: skipping a round

continue does not leave the loop: it abandons the current round and jumps straight to the next one. Everything that came after it in the body is ignored, in that iteration only.

total_workdays = 0

for i in range(4):
    days_text = input(f"Workdays for task {i + 1}: ").strip()
    if not days_text.isdigit():
        print("  Non-numeric value: task left out of the total.")
        continue
    total_workdays += int(days_text)
    print(f"  Running total: {total_workdays} workdays")

print(f"Valid total: {total_workdays} workdays")

With the inputs 4, eight, 2 and 1, the second round prints the warning and skips; the other three accumulate. The total is 7.

continue is useful for discarding cases at the top of the body and leaving the rest of the code unindented. It is the loop equivalent of the guard clauses you saw in 03-01. The alternative without continue would be to wrap the whole body in an if, which works just the same but adds a level of indentation to every line.

break continue
What it does Ends the whole loop Ends only the current round
Where it jumps to The instruction after the loop The next iteration
Typical use I already have what I was after; cancelling Discarding an element and carrying on

  1. The else clause of loops

This is a Python quirk that surprises even programmers with experience in other languages: both for and while accept a final else. And it does not mean what it looks like.

The else of a loop runs if the loop ended naturally, that is, by exhausting the sequence (for) or by the condition turning false (while); it does not run if the loop ended because of a break. The easiest way to remember it is to read it as an else for break: "if the loop finished without being broken, do this". Its canonical use case is the search that found nothing:

TEAM = ("Marta", "Luis", "Nuria")
wanted = "Nuria"

for i in range(3):
    assignee = input(f"Assignee of task {i + 1}: ").strip().capitalize()
    if assignee == wanted:
        print(f"Found a task for {wanted} at position {i + 1}.")
        break
else:
    print(f"None of the 3 tasks belongs to {wanted}.")

If Nuria turns up on some round, the break cuts the loop short and the else does not run. If the three rounds are used up without finding her, the loop ends naturally and the else reports that the search came up empty.

Without this clause we would have to fall back on a flag: initialise found = False, set it to True alongside the break, and check if not found: on leaving the loop. Both versions are correct: the else one is shorter, the flag one is more explicit and can be understood without knowing about this quirk. Many teams prefer the second one for precisely that reason; for...else is one of those constructs you have to know how to read even if you decide never to write it.

while...else works the same way and is even rarer: it runs if the while ended because its condition turned false, not if it left through a break. We will use it in EasyTask for the attempt limit.

  1. Nested loops

A loop can contain another one. The inner one runs in full on every round of the outer one, so the total number of iterations is the product of the two.

for table in range(2, 4):
    for multiplier in range(1, 4):
        print(f"{table} x {multiplier} = {table * multiplier}")
    print("---")

It prints the three multiplications of the 2 times table, a separator, the three of the 3 times table and another separator: six rounds of the inner loop within two of the outer one. And notice the print("---"): it is indented at the level of the outer loop, so it runs once per table, not once per multiplication. In nested loops, indentation is everything.

The realistic case for Alba Studio is a grid of days by person: the team's weekly plan.

TEAM = ("Marta", "Luis", "Nuria")
for day in range(1, 6):
    print(f"--- Day {day} ---")
    for position in range(3):
        person = TEAM[position]
        hours = input(f"Hours for {person}: ").strip()
        print(f"  {person}: {hours} h")

Here TEAM[position] reaches the element at that position of the tuple, counting from 0; it is the first time we do this and we will look at it properly in Lists and arrays. The outer loop does 5 rounds and the inner one does 3 on each: 15 questions in total.

And there lies the cost of nesting. With one loop, doubling the data doubles the work; with two nested loops, doubling the data quadruples it, because the total is the product. Five days by three people is 15 rounds, but 100 days by 100 people is 10,000. That relationship between the size of the problem and the work it takes is called complexity, and it is studied in Efficiency and Big-O notation.

  1. Sentinels and flags for leaving nested loops

An important limitation shows up here: break only breaks the loop it is in. If you are inside the inner loop and want to leave both, a single break is not enough: it leaves the inner one and the outer one keeps going round. The usual solution is a flag that the outer loop checks:

TEAM = ("Marta", "Luis", "Nuria")
cancelled = False
for day in range(1, 6):
    for position in range(3):
        hours = input(f"Hours for {TEAM[position]} on day {day} (x to quit): ").strip()
        if hours.lower() == "x":
            cancelled = True
            break                    # leaves the inner loop
    if cancelled:
        print("Planning cancelled.")
        break                        # leaves the outer loop

The mechanism has three pieces: the flag is initialised to False before everything, it is set to True right before the inner break, and the outer loop checks it as soon as the inner one finishes. The "x" the user types is the sentinel: an agreed value that is not valid data and whose only job is to signal the end.

A sentinel must meet two conditions: it cannot be mistaken for legitimate data (if the hours can be 0, then 0 is no good as a sentinel) and it must be announced in the message the user sees. A secret sentinel is data lost.

  1. match / case: classifying elegantly

Since Python 3.10 there is match, designed for the very specific case of comparing one value against several alternatives. It is exactly what a chain of elif with the same repeated == does, but it reads much better.

priority = "medium"
match priority:
    case "high":
        message = "Deadline: today."
    case "medium":
        message = "Deadline: this week."
    case "low":
        message = "Deadline: whenever there is a gap."
    case _:
        message = "Unknown priority."

print(message)      # Deadline: this week.

There are four rules. match value: opens the structure with the value that is going to be compared; each case puts forward a pattern and only the first one that matches runs; case _: is the default case, equivalent to the else, because the underscore means "anything at all"; and once the chosen case is over the flow leaves the match, with no need to write any break of the kind C or Java demand.

A case can gather several alternatives with the vertical bar |, which here reads as "or":

match priority:
    case "high" | "urgent" | "critical":
        message = "Deadline: today."
    case "medium" | "normal":
        message = "Deadline: this week."
    case _:
        message = "Deadline: whenever there is a gap."

That single line replaces if priority == "high" or priority == "urgent" or priority == "critical":, and makes it obvious that the three words are synonyms for the same thing.

if / elif match / case
Compares Any boolean condition One value against patterns
Ranges and comparisons (days > 15) Yes Not naturally
Several alternatives per branch in (...) or several or Pattern with `
Default case else case _
Minimum Python version Any 3.10

The rule for choosing is simple: if you are comparing one value against a list of specific possibilities, match; if the conditions are calculations or ranges (days > 15, hours <= 40), if / elif. And if your code has to run on Python 3.9 or earlier, match does not exist.

  1. Good practice in flow control

These tools are paid for in readability, so a code of conduct is worth having.

  • No gratuitous break. If the exit condition fits in the header of the while, write it there: while attempts < 3: is more honest than while True: with a break hidden twenty lines further down. And a single exit point per loop wherever possible, because three break statements scattered about force you to read the whole body to know when it ends.
  • Always an attempt limit. A loop that asks until it gets a valid value can go on asking forever. Three attempts and a dignified exit is a better experience than an endless interrogation.
  • Careful with nesting. Two levels are manageable; three, suspicious. Beyond that, rethink the problem.

  1. EasyTask v0.5.3: three attempts and classification with match

Version 0.5.2 insisted indefinitely. Now we apply the fourth piece of good practice: three attempts and we give up with a default value that is announced, not invented in secret. And the priority classification moves to a match.

# easytask.py - Alba Studio
# Version 0.5.3: attempt limit and classification with match

WIDTH = 46
MAX_ATTEMPTS = 3
PRIORITIES = ("high", "medium", "low")
TEAM = ("Marta", "Luis", "Nuria")

title = input("Task title                 : ").strip()
title = title if title != "" else "(no title)"
assignee = input("Assignee (Marta/Luis/Nuria): ").strip().capitalize()
while assignee not in TEAM:
    assignee = input("Not on the team. Assignee  : ").strip().capitalize()

# --- Priority: three attempts and we give up ---
attempts = 0
priority = ""
while attempts < MAX_ATTEMPTS:
    priority = input(f"Priority (high/medium/low) [{attempts + 1}/{MAX_ATTEMPTS}]: ").strip().lower()
    if priority in PRIORITIES:
        break
    print(f"  '{priority}' is not valid.")
    attempts += 1
else:
    priority = "medium"
    print(f"The {MAX_ATTEMPTS} attempts are used up. Priority 'medium' is assigned.")

# --- Deadline classification with match ---
match priority:
    case "high":
        deadline = "Deliver today"
    case "medium":
        deadline = "Deliver this week"
    case "low":
        deadline = "Deliver when there is a gap"
    case _:
        deadline = "Deadline to be decided"

print("=" * WIDTH)
print(f"{'Title':<16}{title:>{WIDTH - 16}}")
print(f"{'Assignee':<16}{f'{assignee} ({priority})':>{WIDTH - 16}}")
print(f"{'Deadline':<16}{deadline:>{WIDTH - 16}}")
print("=" * WIDTH)

Here is the key piece from section 3, now making full sense: the while has an else that only runs if the three attempts were used up. If Marta gets it right on the second go, the break cuts the loop short and the else never runs, so the priority keeps the value she typed. If she fails three times, the condition attempts < MAX_ATTEMPTS turns false by itself, the loop ends naturally and the else applies the default value while saying so.

Priority (high/medium/low) [1/3]: superurgent
  'superurgent' is not valid.
Priority (high/medium/low) [2/3]: very high
  'very high' is not valid.
Priority (high/medium/low) [3/3]: highest
  'highest' is not valid.
The 3 attempts are used up. Priority 'medium' is assigned.
==============================================
Assignee                        Nuria (medium)
Deadline                     Deliver this week
==============================================

Compare the three versions of the same problem and you will see how the judgement evolved: 0.5.1 corrected the value in silence, 0.5.2 demanded without limit and 0.5.3 negotiates: it insists a reasonable number of times and, if there is no way through, it carries on saying exactly what it has decided and why.

Common Mistakes and Tips

Thinking a loop's else runs when the loop does no rounds. It does not: it runs when the loop is not broken by a break. A for with zero rounds runs its else, because it ended naturally.

Expecting one break to leave two loops. It only breaks the loop that directly contains it. To leave a nest you need a flag and one break per level.

Putting continue before the update in a while. It is the most treacherous infinite loop of all: continue jumps to the condition without having incremented the counter. In a for it does not happen, because Python takes care of the control variable.

Getting the order of the case clauses wrong. The first match wins, so a case _: placed at the top makes all the others unreachable. And case days > 15: is not a valid pattern: ranges are if / elif territory.

Mixing up the indentation level in nested loops. An instruction indented one level too far runs N times instead of once. It is the mistake that produces the most bizarre results, and it gives no warning at all.

Tip: name your flags as questions. cancelled, found, has_urgent. That way if cancelled: reads like a sentence and the code explains itself.

Tip: put a safety counter in your while True loops, even if you believe the exit is guaranteed. A while True with no reachable break is a hung program, and in production that is an incident.

Exercises

Exercise 1: Predict the output

Without running anything, say what each fragment prints.

# a)
for i in range(5):
    if i == 3:
        break
    print(i, end=" ")
else:
    print("complete")
# b)
for i in range(5):
    if i % 2 == 0:
        continue
    print(i, end=" ")
else:
    print("complete")
# c)
priority = "urgent"
match priority:
    case "high" | "urgent":
        print("today")
    case _:
        print("another day")

Exercise 2: Finder for Nuria's tasks

Write find_nuria.py which asks how many tasks there are in the batch and then asks, one by one, for the assignee of each task. As soon as it finds the first one belonging to Nuria it must print which position it is in and stop asking. If it goes through them all without finding her, it must report that Nuria has no tasks assigned, using the else clause of the for. Discard empty entries with continue, without letting them count as a valid answer.

Exercise 3: Weekly grid with cancellation

Write week_grid.py which walks with nested loops through the 5 working days and the 3 people in TEAM, asking for the hours planned for each person on each day. It must accumulate the total hours for the week, allow cancelling at any moment by typing x (leaving both loops with a flag) and, when it finishes, classify the total workload with match over a calculated value: "light" below 80 hours, "normal" up to 110 and "excessive" above that. Hint: work out the category first with if/elif and use match for the message.

Solutions

Solution 1.

Output Why
a 0 1 2 The break at i == 3 cuts the loop short, so the else does not run
b 1 3 complete continue skips the even numbers; the loop ends without a break, so the else does run
c today The pattern "high" | "urgent" accepts both words

The contrast between a and b is the whole lesson of the loop else: what decides whether it runs is not how many rounds happened, but whether there was a break.

Solution 2.

# find_nuria.py - Alba Studio
WANTED = "Nuria"
task_count = int(input("How many tasks are there in the batch? "))

for i in range(task_count):
    assignee = input(f"Assignee of task {i + 1}: ").strip().capitalize()
    if assignee == "":
        print("  Empty entry, ignored.")
        continue
    if assignee == WANTED:
        print(f"First task for {WANTED}: number {i + 1}.")
        break
else:
    print(f"{WANTED} has no task assigned in this batch.")

Watch out for one subtlety in the brief: the continue ignores the empty entry, but the round is used up all the same, because the for advances through its sequence whatever happens. If you wanted an empty entry not to spend a turn, you would need a while with your own counter, which you would only increment after a valid answer.

Solution 3.

# week_grid.py - Alba Studio
TEAM = ("Marta", "Luis", "Nuria")
total_hours = 0
cancelled = False

for day in range(1, 6):
    for position in range(3):
        person = TEAM[position]
        entry = input(f"Day {day} - hours for {person} (x to quit): ").strip().lower()
        if entry == "x":
            cancelled = True
            break
        if entry.isdigit():
            total_hours += int(entry)
    if cancelled:
        break

if total_hours < 80:
    category = "light"
elif total_hours <= 110:
    category = "normal"
else:
    category = "excessive"

match category:
    case "light":
        print(f"{total_hours} h: light week, room for more work.")
    case "normal":
        print(f"{total_hours} h: normal week, healthy planning.")
    case "excessive":
        print(f"{total_hours} h: excessive week, the load must be shared.")

Notice how the roles are shared out, which is the underlying message of this lesson: the if / elif takes care of the numeric ranges, because match cannot compare them; and match takes care of turning the already calculated category into a message, which is exactly comparing one value against three possibilities. Each tool doing its own job.

Conclusion

You now control the flow precisely. You know how to cut a loop short with break — including the while True pattern with a controlled exit — and how to discard a round with continue, which is the guard clause of loops. You understand the else clause of for and while, which does not mean "if there were no rounds" but "if there was no break", and you know its canonical case: the search that ends without finding anything.

You know how to nest loops to walk through two-dimensional grids — days by person —, you are clear that the total work is the product of the two (with the cost warning that the efficiency lesson will pick up again), and you know how to leave a nest with a flag and one break per level, signalling the end with a sentinel announced to the user. And you can handle match / case with its | patterns and its case _, knowing that it is for comparing one value against specific alternatives, never for ranges.

EasyTask is at version 0.5.3 and has learnt to negotiate: it gives three attempts, warns on each failure, and if they run out it applies a default value and says so out loud. The else clause of the while tells "she got it right" apart from "it gave up" exactly, and a match turns the priority into a delivery deadline.

And yet the program is still a one-act script: it runs from top to bottom, registers one task and dies. If Marta wants to look at the card she has just created, or change the priority she got wrong, she has no choice but to launch it again and type everything in from scratch. What it lacks is the thing that turns a script into an application: a menu that appears, waits for an order, carries it out and appears again. You already have all the pieces — while True, break, validation, if/elif and match — and in Interactive menus and application loops we will put them together to close the module.

© Copyright 2026. All rights reserved