Control Flow
Before you start
You need: variables and types (Article 01).
Time: about 45 minutes, plus the practice.
Learning objective
Write correct conditions and loops, and choose the right construct instead of reaching for while or an index every time.
Topics
if,elif,else- Conditional expressions
matchforand iterationrangewhilebreak,continue,else- Comprehensions
- Enumerate, zip and unpacking
if, elif, else
if percentage >= 90:
grade = "A+"
elif percentage >= 80:
grade = "A"
elif percentage >= 70:
grade = "B"
elif percentage >= passing_marks:
grade = "C"
else:
grade = "Fail"
elif, not else if. The colon and indentation replace braces.
Order matters. Conditions are tested top to bottom and the first match wins, so the absent check must come before any grade check:
if result.is_absent:
grade = "Absent"
elif result.marks_obtained >= passing_marks:
grade = "Pass"
else:
grade = "Fail"
Reversing the first two lines marks an absent student — whose marks are None — as having failed an exam they never sat. That is the same absent-versus-zero bug this curriculum keeps returning to, and it appears on a real result sheet.
Guard clauses
# Nested — hard to follow
def save_student(student):
if student is not None:
if student.name:
if student.roll_number:
repository.save(student)
# Guard clauses — flat
def save_student(student):
if student is None:
raise ValueError("student is required")
if not student.name:
raise ValueError("name is required")
if not student.roll_number:
raise ValueError("roll number is required")
repository.save(student)
Return or raise early. The happy path ends up unindented at the bottom, which is far easier to read than four levels of nesting.
Conditional expressions
status = "Active" if student.is_active else "Inactive"
display = student.address if student.address else "Not recorded"
display = student.address or "Not recorded" # shorter, same result
X if condition else Y is Python's ternary, written in that order deliberately — the common case reads first.
or for a default is concise and has the usual trap: it also replaces 0, "" and False. When those are valid values, test for None:
marks = result.marks_obtained if result.marks_obtained is not None else 0
match
Python 3.10 and later. More capable than a C# switch.
match exam_type:
case "UnitTest" | "Assignment":
weight = 0.2
case "MidTerm":
weight = 0.3
case "Final" | "Practical":
weight = 0.5
case _:
raise ValueError(f"Unknown exam type: {exam_type}")
case _ is the default. Include it — without one, an unmatched value falls through silently and the variable keeps whatever it held before.
It destructures, which is where it beats a chain of elif:
match student:
case {"name": str(name), "marks": int(marks)} if marks >= 35:
print(f"{name} passed with {marks}")
case {"name": str(name), "is_absent": True}:
print(f"{name} was absent")
case _:
print("Unrecognised record")
match point:
case (0, 0):
print("Origin")
case (x, 0):
print(f"On the x-axis at {x}")
case (x, y):
print(f"At {x}, {y}")
For a simple value comparison, elif is clearer. Reach for match when you are matching shape as well as value.
for
Python's for iterates a sequence directly — there is no C-style index loop.
for student in students:
print(student.name)
for character in "Ravi":
print(character)
for key, value in student_dict.items():
print(f"{key}: {value}")
# C-style, translated badly
for i in range(len(students)):
print(students[i].name)
# Idiomatic
for student in students:
print(student.name)
Iterating by index is the clearest sign of C# or Java habits. Use it only when you genuinely need the index — and then use enumerate.
range
range(5) # 0, 1, 2, 3, 4
range(1, 6) # 1, 2, 3, 4, 5
range(0, 10, 2) # 0, 2, 4, 6, 8
range(10, 0, -1) # 10 down to 1
range excludes the stop value. range(1, 6) gives 1 to 5, so a loop over class numbers 1 to 12 is range(1, 13).
range produces values lazily — range(1_000_000) allocates nothing until iterated.
enumerate
for index, student in enumerate(students):
print(f"{index + 1}. {student.name}")
for index, student in enumerate(students, start=1):
print(f"{index}. {student.name}")
start=1 removes the + 1 from the body — clearer for a numbered list.
zip
names = ["Ravi Kumar", "Priya Sharma", "Arjun Reddy"]
marks = [87, 91, 65]
for name, mark in zip(names, marks):
print(f"{name}: {mark}")
zip stops at the shortest sequence, silently. A names list longer than a marks list drops the extras with no warning:
for name, mark in zip(names, marks, strict=True): # Python 3.10+ — raises on mismatch
...
Use strict=True whenever the lengths are supposed to match.
Modifying while iterating
# Wrong — skips elements
for student in students:
if student.status == "Inactive":
students.remove(student)
# Right — build a new list
students = [s for s in students if s.status != "Inactive"]
# Or iterate a copy
for student in students[:]:
if student.status == "Inactive":
students.remove(student)
Removing during iteration shifts the remaining items, so the loop skips one each time. The result is a list that still contains inactive students — and it looks like the filter is broken.
while
attempts = 0
max_attempts = 3
while attempts < max_attempts:
password = input("Password: ")
if verify(password):
break
attempts += 1
else:
print("Too many failed attempts.")
Use while when the number of iterations is not known in advance — retries, reading until a sentinel, a menu loop.
Ensure the condition can become false. A while whose body never changes the condition hangs the program, and Python gives no warning.
while True:
choice = input("Choice: ").strip()
if choice == "5":
break
handle(choice)
while True with a break is idiomatic for a menu — Python has no do-while.
break, continue and else
for student in students:
if student.status != "Active":
continue # skip to the next iteration
if student.roll_number == target:
found = student
break # leave the loop
The loop else
for student in students:
if student.roll_number == target:
print(f"Found {student.name}")
break
else:
print("No student with that roll number.")
else on a loop runs when the loop finished without break. It is unique to Python and frequently misread as "runs otherwise".
It replaces the found = False flag pattern, and it is worth knowing because you will meet it — but a comment helps, since most readers pause at it.
Comprehensions
The most idiomatic construct in the language.
# Loop
active_names = []
for student in students:
if student.status == "Active":
active_names.append(student.name)
# Comprehension
active_names = [s.name for s in students if s.status == "Active"]
[expression for item in iterable]
[expression for item in iterable if condition]
[a if condition else b for item in iterable]
Note the position: a filtering if goes at the end; a conditional expression goes at the front.
percentages = [r.marks_obtained / r.max_marks * 100 for r in results]
passed = [r for r in results if not r.is_absent and r.marks_obtained >= 35]
labels = [f"{s.name} ({s.roll_number})" for s in students]
grades = ["Pass" if r.marks_obtained >= 35 else "Fail" for r in results if not r.is_absent]
Dict and set comprehensions:
by_roll = {s.roll_number: s for s in students}
class_names = {s.class_name for s in students} # unique, unordered
marks_by_name = {s.name: s.marks for s in students if s.marks is not None}
{s.roll_number: s for s in students} is the standard way to build a lookup — and it replaces the nested-loop join that turns O(n) into O(n × m).
A generator expression uses parentheses and produces values lazily:
total = sum(r.marks_obtained for r in results if not r.is_absent)
any(s.status == "Inactive" for s in students)
all(r.marks_obtained >= 35 for r in results)
Use a generator inside sum, any, all and max. It builds no intermediate list, and for any/all it stops at the first decisive item.
When not to use one
# Unreadable — use a loop
result = [transform(x) for sublist in matrix for x in sublist
if condition(x) and other(x) or fallback(x)]
A comprehension should fit on one or two lines and do one thing. Two for clauses plus a compound condition is a loop written badly.
Never use one purely for a side effect:
# Wrong — builds a list of None just to print
[print(s.name) for s in students]
# Right
for student in students:
print(student.name)
Unpacking
first, second, third = students
name, roll = "Ravi Kumar", "NCA-2024-0012"
a, b = b, a # swap, no temporary
first, *rest = students
*most, last = students
school, year, sequence = roll_number.split("-")
for index, (name, marks) in enumerate(zip(names, marks_list), start=1):
print(f"{index}. {name}: {marks}")
Unpacking makes loops over pairs read naturally, and the *rest form avoids slicing.
Errors you will hit
| Message | Cause | Fix |
|---|---|---|
IndentationError: expected an indented block | Body missing after if/for/def | Indent the body |
if marks: skips a genuine 0 | 0 is falsy | Test if marks is not None: |
TypeError: '<' not supported between 'NoneType' and 'int' | Compared None with a number | Check for None first |
| Loop variable leaks after the loop | Python scoping — expected | Do not rely on it |
RuntimeError: dictionary changed size during iteration | Modified while looping | Iterate a copy |
if marks: is false for 0. For anything where zero is a real value, test is not None — the same absent-versus-zero rule as everywhere else in this curriculum.
Common mistakes
else ifinstead ofelif- The absent check placed after the grade checks
range(1, 12)when 12 should be included- Iterating by index instead of by item
zipsilently truncating withoutstrict=True- Removing from a list while iterating it
- A
whilewhose condition never changes - Misreading loop
elseas "runs otherwise" - No
case _in amatch - A comprehension used for side effects
- A comprehension too complex to read
orfor a default where0or""are valid- Building a list where a generator would do
Practice
The course exercise is solve logic problems.
- Write a grading function with the absent check first. Reverse the order and confirm an absent student is marked Fail.
- Rewrite a four-level nested validation as guard clauses.
- Write a menu loop with
while Trueandbreak. - Loop over class numbers 1 to 12. Confirm
range(1, 12)misses one. - Rewrite an index loop as a direct
for. Then useenumeratewhere the index is genuinely needed. ziptwo lists of different lengths. Confirm the silent truncation, then addstrict=True.- Remove inactive students while iterating. Confirm some are skipped, then fix it two ways.
- Use loop
elseto report "not found" after a search. - Convert three loops into comprehensions.
- Build a
{roll_number: student}lookup with a dict comprehension. - Sum marks with a generator expression, then with a list comprehension. Compare memory with
sys.getsizeof. - Write a
matchon exam type withcase _. Remove the default and pass an unknown value. - Use
matchto destructure a dictionary with a guard condition. - Unpack a roll number into three variables with
split. - Write a comprehension with two
forclauses and a compound condition, then rewrite it as a loop and compare readability.
Exercises 1, 6 and 7 correspond to three real defects.
You can now
- Write correct conditions and loops
- Say why
if x:is wrong when0is valid - Use comprehensions where they read better than loops
- Avoid modifying a collection while iterating it
- Handle
Nonebefore comparing
Review questions
- Why must the absent check come first in a grading chain?
- What does
elseon aforloop actually mean? - Why does removing items while iterating skip elements?
- When is a generator expression better than a list comprehension?
Next: Collections