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
break: leaving the loop earlycontinue: skipping a round- The
elseclause of loops - Nested loops
- Sentinels and flags for leaving nested loops
match/case: classifying elegantly- Good practice in flow control
- EasyTask v0.5.3: three attempts and classification with
match - Common mistakes and tips
- Exercises
- Conclusion
break: leaving the loop early
break: leaving the loop earlybreak 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.
continue: skipping a round
continue: skipping a roundcontinue 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 |
- The
else clause of loops
else clause of loopsThis 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.
- 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.
- 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 loopThe 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.
match / case: classifying elegantly
match / case: classifying elegantlySince 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.
- 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 thewhile, write it there:while attempts < 3:is more honest thanwhile True:with abreakhidden twenty lines further down. And a single exit point per loop wherever possible, because threebreakstatements 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.
- EasyTask v0.5.3: three attempts and classification with
match
matchVersion 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.
Fundamentals of Programming
Module 1: Introduction to Programming
- What is programming?
- History of programming
- Programming languages
- Development environments
- From problem to algorithm
Module 2: Core Concepts
- Variables and data types
- Operators and expressions
- Input and output
- Type conversion and data validation
Module 3: Control Structures
Module 4: Functions and Procedures
- Defining and using functions
- Parameters and return values
- Variable scope
- Breaking a program down into functions
- Functions as values: lambda and higher order
Module 5: Data Structures
- Lists and arrays
- Strings
- Dictionaries and sets
- Tuples and nested structures
- Saving data to files: text, CSV and JSON
Module 6: Basic Algorithms
Module 7: Objects and Code Organisation
- From data to objects: classes and instances
- Attributes, methods and the constructor
- Collections of objects
- Modules, packages and imports
Module 8: Good Practices and Tools
- Documentation and comments
- Debugging and error handling
- Version control
- Automated testing
- Style, readability and refactoring
