Learning Objectives¶
By the end of this seminar you will be able to:
Understand what control flow is and why algorithms depend on it
Write
if/elif/elsebranches and reason about truth tablesUse
forloops withrange(),enumerate(), andzip()Use
whileloops safely, knowing when to prefer them overforControl loop execution with
break,continue, andpassRecognise nested-loop complexity (informally: O(n²))
Write basic list comprehensions
Part 1: Theory¶
2.1 What Is Control Flow?¶
By default, Python executes statements top to bottom, one after another. Control flow changes that default order: it lets the program make decisions, repeat actions, and skip over code.
Every algorithm is ultimately just:
Sequence — do A, then B, then C
Selection — if condition X, do A; otherwise do B
Iteration — repeat A until condition Y is met
These three constructs are all you need to express any computable function (this is called Turing completeness).
Why does it matter for algorithms?¶
The structure of your control flow directly determines:
Correctness — does the algorithm produce the right answer?
Efficiency — how many operations does it perform?
For example, linear search scans every element (one loop), while binary search halves the search space each time (loop + branch). Same problem, radically different performance.
Control Flow at a Glance
─────────────────────────────────────────────────
Sequence: A → B → C
Selection: condition? → A (yes) / B (no)
Iteration: condition? → A → back to condition
─────────────────────────────────────────────────2.2 if / elif / else — Branching¶
Syntax¶
if condition_1:
# executed when condition_1 is True
elif condition_2:
# executed when condition_1 is False AND condition_2 is True
else:
# executed when ALL conditions above are FalseThe
elifandelseclauses are optional.You can have multiple
elifclauses.Conditions are any expression that evaluates to a truthy or falsy value.
Truthiness in Python¶
Use this quick reference to understand which values Python treats as False and which ones it treats as True in conditions.
Falsy (treated as False) | Truthy (treated as True) |
|---|---|
False, None | True |
0, 0.0 | Any non-zero number |
"", '' | Any non-empty string |
[], (), {}, set() | Any non-empty container |
Comparison operators¶
These operators let you compare values and form conditions that drive branching logic.
| Operator | Meaning |
|---|---|
== | Equal to |
!= | Not equal to |
<, > | Less / greater than |
<=, >= | Less / greater than or equal |
is | Identity (same object in memory) |
in | Membership test |
Beginner tip: use
==to compare values (e.g.x == 5). Useisfor identity checks (most commonlyvalue is None).
Boolean operators: truth table¶
This truth table summarizes how and, or, and not combine or invert boolean values.
A | B | A and B | A or B | not A |
|---|---|---|---|---|
| T | T | T | T | F |
| T | F | F | T | F |
| F | T | F | T | T |
| F | F | F | F | T |
# --- Basic if/elif/else ---
def classify_grade(score):
"""Return a letter grade for a score in [0, 100]."""
if score >= 90:
return "A"
elif score >= 80:
return "B"
elif score >= 70:
return "C"
elif score >= 60:
return "D"
else:
return "F"
scores = [95, 83, 71, 64, 50, 100, 0]
for s in scores:
print(f"Score {s:>3} → Grade {classify_grade(s)}")
print()
# --- Chained comparisons (Python-specific, very readable) ---
x = 15
if 10 <= x <= 20: # equivalent to: 10 <= x and x <= 20
print(f"{x} is between 10 and 20 (inclusive)")
# --- Ternary / conditional expression (one-liner if/else) ---
temperature = 22
weather = "warm" if temperature >= 20 else "cold"
print(f"Temperature {temperature}°C is {weather}")# --- Interactive demo: which branch executes? ---
import ipywidgets as widgets
from IPython.display import display, HTML
def show_branch(number):
"""Show which if/elif/else branch is taken for a given number."""
output_lines = [f"<b>Input number: {number}</b><br>"]
output_lines.append("<pre style='font-family:monospace; font-size:13px;'>")
# We manually trace through each condition
if number > 0:
branch = "POSITIVE"
colour = "#2ecc71"
elif number < 0:
branch = "NEGATIVE"
colour = "#e74c3c"
else:
branch = "ZERO"
colour = "#3498db"
# Build a visual "code trace"
conditions = [
(f"if {number} > 0:", number > 0),
(f"elif {number} < 0:", number < 0),
("else:", not (number > 0 or number < 0)),
]
for code_line, taken in conditions:
marker = " ← TAKEN" if taken else ""
style = f"color:{colour}; font-weight:bold;" if taken else "color:#888;"
output_lines.append(
f"<span style='{style}'>{code_line}{marker}</span><br>"
)
output_lines.append("</pre>")
output_lines.append(
f"<p style='color:{colour}; font-size:15px;'>Result: <b>{branch}</b></p>"
)
display(HTML("".join(output_lines)))
slider = widgets.IntSlider(
value=0, min=-20, max=20, step=1,
description="Number:",
style={"description_width": "initial"},
layout=widgets.Layout(width="55%")
)
out = widgets.interactive_output(show_branch, {"number": slider})
display(widgets.Label("Move the slider and watch which branch is taken:"), slider, out)2.3 for Loops — Definite Iteration¶
A for loop iterates over any iterable (list, string, range, dict, file, …). You know in advance (conceptually) how many iterations will occur.
range()¶
| Call | Values produced |
|---|---|
range(5) | 0, 1, 2, 3, 4 |
range(2, 7) | 2, 3, 4, 5, 6 |
range(0, 10, 2) | 0, 2, 4, 6, 8 |
range(5, 0, -1) | 5, 4, 3, 2, 1 |
enumerate() — loop with index¶
Use enumerate() instead of range(len(...)) — it is more Pythonic and less error-prone.
# BAD (C-style, avoid in Python):
for i in range(len(items)):
print(items[i])
# GOOD:
for i, item in enumerate(items):
print(i, item)zip() — loop over multiple iterables¶
names = ["Alice", "Bob"]
scores = [95, 87]
for name, score in zip(names, scores):
print(f"{name}: {score}")# --- range() examples ---
print("range(5):", list(range(5)))
print("range(2, 7):", list(range(2, 7)))
print("range(0,10,2):", list(range(0, 10, 2)))
print("range(5,0,-1):", list(range(5, 0, -1)))
print()
# --- Iterating over a list ---
fruits = ["apple", "banana", "cherry", "date"]
print("Direct iteration:")
for fruit in fruits:
print(f" {fruit}")
print()
# --- enumerate() ---
print("With enumerate():")
for index, fruit in enumerate(fruits):
print(f" [{index}] {fruit}")
# enumerate() with a start index
print("\nStarting from index 1:")
for rank, fruit in enumerate(fruits, start=1):
print(f" #{rank}: {fruit}")
print()
# --- zip() ---
names = ["Alice", "Bob", "Carol"]
scores = [95, 87, 72]
grades = ["A", "B", "C"]
print("With zip():")
for name, score, grade in zip(names, scores, grades):
print(f" {name:>6}: {score} ({grade})")
print()
# --- Iterating over a string (strings are iterables!) ---
word = "Python"
print(f"Letters in '{word}': ", end="")
for ch in word:
print(ch, end=" ")
print()
# --- Iterating over a dict ---
capitals = {"France": "Paris", "Japan": "Tokyo", "Brazil": "Brasília"}
print("\nCountry capitals:")
for country, capital in capitals.items():
print(f" {country}: {capital}")2.4 while Loops — Indefinite Iteration¶
Use a while loop when you do not know in advance how many iterations you need — only the stopping condition.
Core: understand the basic counter-style
whileloop first.Bonus: Collatz and Newton’s method are enrichment examples; skip on first pass if needed.
while condition:
# body — runs as long as condition is TrueWhen to choose while over for¶
| Situation | Use |
|---|---|
| Known number of iterations | for |
| Iterating over a collection | for |
| Waiting for user input / event | while |
| Algorithm converges (e.g. Newton’s method) | while |
| Game loop | while |
Danger: infinite loops¶
If the condition never becomes False, the loop runs forever. Always ensure:
The loop variable is updated inside the body.
The condition will eventually be met.
You have a
breakas an emergency exit if needed.
import math
# --- CORE: Basic while loop ---
counter = 0
while counter < 5:
print(f" counter = {counter}")
counter += 1 # IMPORTANT: increment to avoid infinite loop
print("Loop finished.")
print()
# --- BONUS: Collatz conjecture (enrichment) ---
# Start with any positive integer n.
# If n is even: divide by 2. If odd: multiply by 3 and add 1.
# Conjecture: it always reaches 1 (unproven for all numbers!).
def collatz(n):
"""Return the Collatz sequence starting at n."""
sequence = [n]
while n != 1:
if n % 2 == 0:
n = n // 2
else:
n = 3 * n + 1
sequence.append(n)
return sequence
for start in [6, 11, 27]:
seq = collatz(start)
print(f"Collatz({start}): {seq}")
print(f" Steps to reach 1: {len(seq) - 1}")
print()
# --- BONUS: Newton's method (enrichment) ---
# Uses while loop because convergence time is not known in advance.
def sqrt_newton(n, tolerance=1e-10):
"""Approximate sqrt(n) using Newton-Raphson iteration."""
if n < 0:
raise ValueError("Cannot take square root of a negative number")
guess = n / 2.0 # initial guess
iterations = 0
while abs(guess * guess - n) > tolerance:
guess = (guess + n / guess) / 2 # Newton step
iterations += 1
return guess, iterations
for num in [2, 9, 100, 12345]:
approx, iters = sqrt_newton(num)
print(f"sqrt({num:>5}): approx={approx:.8f}, math.sqrt={math.sqrt(num):.8f}, iters={iters}")2.5 break, continue, pass¶
| Statement | Effect |
|---|---|
break | Exit the innermost loop immediately |
continue | Skip the rest of the current iteration; go to the next one |
pass | Do nothing — a syntactic placeholder (empty block) |
else clause on loops¶
Bonus: this pattern is useful, but optional on your first read.
Python has a unique feature: for/while loops can have an else clause that runs only if the loop completed normally (i.e., was not terminated by break).
for item in collection:
if condition:
break # skip the else
else:
# runs only if break was never hit
print("No item matched")This is very useful for search algorithms.
# --- break: stop the loop early ---
print("=== break ===")
for i in range(10):
if i == 5:
print(f" Found 5! Breaking out of loop.")
break
print(f" i = {i}")
print()
# --- continue: skip even numbers ---
print("=== continue (printing only odd numbers) ===")
for i in range(10):
if i % 2 == 0:
continue # skip even
print(f" {i}")
print()
# --- pass: placeholder in an empty branch ---
print("=== pass ===")
for i in range(5):
if i == 2:
pass # placeholder — do nothing for 2 (no error without a body)
else:
print(f" Processing {i}")
print()
# --- for/else: prime checking ---
print("=== for...else: prime checking ===")
def is_prime(n):
"""Return True if n is prime, False otherwise."""
if n < 2:
return False
for divisor in range(2, int(n ** 0.5) + 1):
if n % divisor == 0:
return False # found a divisor — not prime
return True # loop completed without break → prime
primes = [n for n in range(2, 30) if is_prime(n)]
print(f" Primes below 30: {primes}")
print()
# Version explicitly using for/else:
def is_prime_explicit(n):
"""Same as above but uses for...else explicitly to illustrate the pattern."""
if n < 2:
return False
for divisor in range(2, int(n ** 0.5) + 1):
if n % divisor == 0:
break # composite — break skips the else
else:
return True # else only runs if no break occurred
return False
test_nums = [2, 7, 9, 13, 25, 29]
for num in test_nums:
print(f" is_prime({num}) = {is_prime_explicit(num)}")2.6 Nested Loops and Complexity¶
A loop inside another loop is called a nested loop. They are necessary for working with 2D data (matrices, grids) or for certain algorithms (bubble sort, matrix multiplication).
Core: focus on the multiplication-table nested loop example first.
Bonus: the growth-rate comparison (
O(n),O(n log n),O(n²)) is conceptual enrichment.
Informally: O(n²)¶
If both the outer and inner loops run n times each, the total number of operations is approximately n × n = n². We write this as O(n²) (Big-O notation — covered formally later in the course).
| n | n (single loop) | n² (nested loop) |
|---|---|---|
| 10 | 10 | 100 |
| 100 | 100 | 10,000 |
| 1,000 | 1,000 | 1,000,000 |
| 10,000 | 10,000 | 100,000,000 |
This is why algorithms like bubble sort (O(n²)) become impractical for large inputs, while merge sort (O(n log n)) scales much better.
# --- CORE: Nested loop: 5×5 multiplication table ---
print("5 × 5 Multiplication Table:")
print(" ", end="")
for j in range(1, 6):
print(f"{j:>4}", end="")
print()
print(" " + "-" * 20)
for i in range(1, 6): # outer loop: rows
print(f"{i:>2} |", end="")
for j in range(1, 6): # inner loop: columns
print(f"{i * j:>4}", end="")
print() # newline after each row
print()
# --- BONUS: Count operations to visualise O(n²) growth ---
import matplotlib.pyplot as plt
ns = list(range(1, 51))
ops_linear = ns # O(n)
ops_quadratic = [n ** 2 for n in ns] # O(n²)
ops_nlogn = [n * (n.bit_length()) for n in ns] # rough O(n log n)
fig, ax = plt.subplots(figsize=(8, 4))
ax.plot(ns, ops_linear, label="O(n) — linear", linewidth=2)
ax.plot(ns, ops_nlogn, label="O(n log n) — merge sort", linewidth=2, linestyle="--")
ax.plot(ns, ops_quadratic, label="O(n²) — nested loop", linewidth=2, linestyle=":")
ax.set_xlabel("Input size n")
ax.set_ylabel("Number of operations (approx.)")
ax.set_title("Growth of Algorithm Complexity", fontweight="bold")
ax.legend()
ax.set_xlim(1, 50)
ax.set_ylim(0)
ax.spines["top"].set_visible(False)
ax.spines["right"].set_visible(False)
plt.tight_layout()
plt.show()2.7 List Comprehensions — Concise Iteration¶
A list comprehension creates a new list by applying an expression to each element of an iterable, optionally filtering with a condition:
Core: learn the plain form and the single
iffilter first.Bonus: flattening patterns, dict/set comprehensions, and performance timing are optional for first pass.
[expression for variable in iterable if condition]Equivalent to a for loop with append(), but more concise and often faster.
# Classic loop approach:
squares = []
for x in range(10):
squares.append(x ** 2)
# List comprehension (equivalent, preferred in Python):
squares = [x ** 2 for x in range(10)]Rule of thumb: if the comprehension fits on one line and is readable, use it. If it requires multiple lines or complex logic, use a regular loop.
# --- CORE: Basic list comprehension ---
squares = [x ** 2 for x in range(1, 11)]
print(f"Squares 1..10: {squares}")
# --- CORE: With condition (filter) ---
even_squares = [x ** 2 for x in range(1, 11) if x % 2 == 0]
print(f"Even squares: {even_squares}")
# --- BONUS: Transforming strings ---
words = ["hello", "world", "python", "algorithmics"]
upper_long = [w.upper() for w in words if len(w) > 5]
print(f"Uppercased long words: {upper_long}")
# --- BONUS: Flattening a 2D list ---
matrix = [[1, 2, 3], [4, 5, 6], [7, 8, 9]]
flat = [elem for row in matrix for elem in row]
print(f"Flattened matrix: {flat}")
# --- BONUS: Dictionary comprehension ---
word_lengths = {word: len(word) for word in words}
print(f"Word lengths: {word_lengths}")
# --- BONUS: Set comprehension ---
unique_lengths = {len(word) for word in words}
print(f"Unique lengths: {unique_lengths}")
# --- BONUS: Performance note ---
# Comprehensions are generally faster than equivalent loops because
# the iteration is optimised at the C level in CPython.
import timeit
loop_time = timeit.timeit(
"result = []\nfor x in range(1000):\n result.append(x**2)",
number=10000
)
comp_time = timeit.timeit(
"result = [x**2 for x in range(1000)]",
number=10000
)
print(f"\nPerformance (10000 runs of squaring 1000 elements):")
print(f" for loop: {loop_time:.4f}s")
print(f" list comprehension:{comp_time:.4f}s")
print(f" Speedup: {loop_time / comp_time:.2f}x")Exercise 1: Maximum of Three Numbers¶
Write a function max_of_three(a, b, c) that returns the largest of three numbers using if/elif/else (do not use the built-in max()).
Examples:
max_of_three(3, 7, 5)->7max_of_three(-2, -9, -1)->-1
Hint: compare one candidate maximum step by step, then return it.
# Exercise 1 — Maximum of three numbers
def max_of_three(a, b, c):
# TODO: implement with if/elif/else
pass
# Quick checks
print(max_of_three(3, 7, 5))
print(max_of_three(-2, -9, -1))Exercise 2: FizzBuzz¶
Print numbers from 1 to n, but:
Print
"Fizz"for multiples of 3Print
"Buzz"for multiples of 5Print
"FizzBuzz"for multiples of both 3 and 5Print the number itself otherwise
Example for n = 16 (start): 1, 2, Fizz, 4, Buzz, ... , 14, FizzBuzz, 16
Hint: use % (modulo) and check the combined case (i % 3 == 0 and i % 5 == 0) first.
Important: check the combined condition (FizzBuzz) first, otherwise it will never be reached.
# Exercise 2 — FizzBuzz
def fizzbuzz(n):
# TODO: implement
pass
# Quick check
fizzbuzz(16)Exercise 3: Leap Year Checker¶
A year is a leap year if:
It is divisible by 4 AND
It is not divisible by 100, unless it is also divisible by 400.
Examples:
is_leap_year(2000)->Trueis_leap_year(1900)->Falseis_leap_year(2024)->Trueis_leap_year(2023)->False
Hint: translate the rule into nested checks or into one combined boolean expression.
Write is_leap_year(year) and test it.
# Exercise 3 — Leap year checker
def is_leap_year(year):
# TODO: implement
pass
# Quick checks
for y in [2000, 1900, 2024, 2023]:
print(y, is_leap_year(y))
Exercise 4: Interactive Calculator (while loop)¶
Build a calculator that repeatedly asks the user for two numbers and an operator, then prints the result. The loop continues until the user types quit.
Supported operations: +, -, *, /
Example interaction:
user enters
+,8,3-> output11user enters
/,8,2-> output4.0user enters
quit-> program stops
Hint: use while True: and break when operator is quit. Handle division by zero with an if check.
# Exercise 4 — Interactive calculator with while loop
while True:
op = input("Operator (+, -, *, /) or 'quit': ").strip()
if op == "quit":
print("Goodbye!")
break
# TODO: read two numbers and apply operation
# a = float(input("First number: "))
# b = float(input("Second number: "))
# ...
Exercise 5: Multiplication Table¶
Write a function multiplication_table(n) that prints the multiplication table for a given number n (from 1 × n to 12 × n).
Example for n = 4 (start):
1 x 4 = 42 x 4 = 8...
12 x 4 = 48
Hint: a for loop over range(1, 13) is enough.
Extension: print a full n × n grid.
# Exercise 5 — Multiplication table
def multiplication_table(n):
# TODO: implement
pass
# Quick check
multiplication_table(4)
Summary¶
| Concept | When to use |
|---|---|
if/elif/else | Decision making based on conditions |
for | Known number of iterations / iterating over a collection |
while | Unknown iterations, event-driven, convergence |
break | Exit loop early (search found / error) |
continue | Skip current item, continue with next |
pass | Syntactic placeholder for empty blocks |
| List comprehension | Concise list creation from iteration |
Next seminar: Data structures — lists, tuples, dictionaries, sets, functions, and classes.