Skip to main content

Python break, continue, Practice Problems & Exercises

Practice: break, continue, and loop else

10 problems3 Easy4 Medium3 Hard35–50 min
← Back to lesson

Easy

#1Find the First Negative NumberEasy
breakearly-exitsearch

Find the first negative number in a list and stop scanning immediately. Print each number as you scan it so you can see that the loop exits early.

Python
numbers = [4, 7, 12, -3, 8, -1, 15]

for num in numbers:
    print(f"Scanning: {num}")
    if num < 0:
        print(f"First negative found: {num}")
        break
Solution
Scanning: 4
Scanning: 7
Scanning: 12
Scanning: -3
First negative found: -3

Key insight: Without break, the loop would continue to scan 8, -1, and 15 — wasting work and printing both -3 and -1. The break statement guarantees we stop at the first match.

Performance implication: In a list of 1 million elements where the first negative is at index 5, break saves you from checking 999,995 unnecessary elements. This is identical to the early-exit pattern in linear search — O(n) worst case but often much faster in practice.

Expected Output
Scanning: 4
Scanning: 7
Scanning: 12
Scanning: -3
First negative found: -3
Hints

Hint 1: Use a for loop over the list and check each number. When you find one that is less than 0, print it and break immediately.

Hint 2: The break statement exits only the innermost loop — once it fires, execution continues with whatever comes after the loop.

#2Sum Only Odd NumbersEasy
continueskip-patternfiltering

Sum only the odd numbers in a list, using continue to skip even numbers. Print what you skip and what you add so the control flow is visible.

Python
numbers = [2, 5, 7, 10, 3, 8, 1, 6]
total = 0

for num in numbers:
    if num % 2 == 0:
        print(f"Skipping even: {num}")
        continue
    print(f"Adding odd: {num}")
    total += num

print(f"Sum of odd numbers: {total}")
Solution
Skipping even: 2
Adding odd: 5
Adding odd: 7
Skipping even: 10
Adding odd: 3
Skipping even: 8
Adding odd: 1
Skipping even: 6
Sum of odd numbers: 16

continue vs if-else: You could write the same logic with if num % 2 != 0: total += num (no continue needed). So when is continue better? When the "skip" condition is checked first and the remaining loop body is long. continue acts as a guard clause inside a loop — it keeps the main logic at a low indentation level.

# Without continue — deeper nesting
for record in records:
if record.is_valid():
if record.has_data():
process(record)
update(record)
log(record)

# With continue — flat and readable
for record in records:
if not record.is_valid():
continue
if not record.has_data():
continue
process(record)
update(record)
log(record)
Expected Output
Skipping even: 2
Adding odd: 5
Adding odd: 7
Skipping even: 10
Adding odd: 3
Skipping even: 8
Adding odd: 1
Skipping even: 6
Sum of odd numbers: 16
Hints

Hint 1: Use continue to skip even numbers (num % 2 == 0). After continue, the rest of the loop body is skipped and the loop advances to the next iteration.

Hint 2: continue does NOT exit the loop — it just skips the current iteration. The loop keeps running until all elements are processed.

#3Predict the Output: for-else with and without breakEasy
for-elsebreakpredict-output

Predict the output before running. Two searches use the same for-else pattern — one finds the target, one does not. When does the else clause execute?

Python
def search(fruits, target):
    print(f"--- Search: looking for '{target}' ---")
    for fruit in fruits:
        print(f"Checking: {fruit}")
        if fruit == target:
            print(f"Found {target}!")
            break
    else:
        print(f"{target} was NOT found in the list")

fruits = ["apple", "banana", "cherry"]

search(fruits, "cherry")
search(fruits, "mango")
Solution
--- Search 1: looking for 'cherry' ---
Checking: apple
Checking: banana
Checking: cherry
Found cherry!
--- Search 2: looking for 'mango' ---
Checking: apple
Checking: banana
Checking: cherry
mango was NOT found in the list

The for-else rule is simple: The else block runs if and only if the loop terminates naturally (exhausts all items). If break fires, the else is skipped entirely.

Mental model — think of else as nobreak:

for item in items:
if condition:
break # <-- if this fires, else is SKIPPED
else:
# runs only if break NEVER fired
# i.e., the loop searched everything and found nothing

Edge case: If the iterable is empty (zero iterations), the else block still runs — because break never fired.

for x in []:
print("never runs")
else:
print("this DOES run — empty loop, no break")
Expected Output
--- Search 1: looking for 'cherry' ---
Checking: apple
Checking: banana
Checking: cherry
Found cherry!
--- Search 2: looking for 'mango' ---
Checking: apple
Checking: banana
Checking: cherry
mango was NOT found in the list
Hints

Hint 1: The else clause of a for loop runs only if the loop completes WITHOUT hitting a break. Think of it as "no-break" — if break fires, else is skipped.

Hint 2: In Search 1, break fires when cherry is found, so else is skipped. In Search 2, the loop finishes all items without break, so else runs.


Medium

#4Linear Search with for-elseMedium
for-elselinear-searchindex-tracking

Implement linear search using for-else. Return the index of the target if found, or -1 if not found. Do not use Python's in operator or list.index().

Python
def linear_search(haystack, needle):
    """Return the index of needle in haystack, or -1 if not found."""
    for i, value in enumerate(haystack):
        if value == needle:
            return i
    else:
        return -1


# Test cases
data = [10, 25, 33, 47, 52, 68, 71]

print(f"Search for 47: index = {linear_search(data, 47)}")
print(f"Search for 10: index = {linear_search(data, 10)}")
print(f"Search for 71: index = {linear_search(data, 71)}")
print(f"Search for 99: index = {linear_search(data, 99)}")
print(f"Search in []:  index = {linear_search([], 5)}")
Solution
def linear_search(haystack, needle):
for i, value in enumerate(haystack):
if value == needle:
return i # Found — exit function (acts like break)
else:
return -1 # Not found — loop completed without returning
Search for 47: index = 3
Search for 10: index = 0
Search for 71: index = 6
Search for 99: index = -1
Search in []: index = -1

Why for-else works perfectly for search: The pattern maps directly to the search problem — "iterate until you find it, and if you never find it, do something else." Without for-else, you would need a flag variable:

# Without for-else — needs a sentinel flag
def linear_search_no_else(haystack, needle):
found_index = -1
for i, value in enumerate(haystack):
if value == needle:
found_index = i
break
return found_index

Subtlety: In this case, return inside the loop acts as both break and value delivery. The else clause runs only when the for loop completes without a return (or break). Since return exits the function entirely, the else clause here is technically equivalent to just putting return -1 after the loop — but the for-else version makes the intent explicit: "if we never found it, return -1."

def linear_search(haystack, needle):
    """Return the index of needle in haystack, or -1 if not found.
    
    Use for-else — do NOT use the 'in' operator or list.index().
    """
    # Your code here
    pass


# Test cases
data = [10, 25, 33, 47, 52, 68, 71]

print(f"Search for 47: index = {linear_search(data, 47)}")
print(f"Search for 10: index = {linear_search(data, 10)}")
print(f"Search for 71: index = {linear_search(data, 71)}")
print(f"Search for 99: index = {linear_search(data, 99)}")
print(f"Search in []:  index = {linear_search([], 5)}")
Expected Output
Search for 47: index = 3
Search for 10: index = 0
Search for 71: index = 6
Search for 99: index = -1
Search in []:  index = -1
Hints

Hint 1: Use enumerate() to get both the index and value as you iterate. When you find a match, return the index and break (return acts as break here — it exits the function).

Hint 2: The else clause of the for loop runs when the needle is not found. Return -1 there. Remember: return inside the loop prevents else from running (it exits the function entirely).

#5Data Cleaning with continueMedium
continuedata-cleaningvalidationguard-clause

Clean a list of records by skipping invalid entries with continue. Each record is a dict with name, age, and email. Skip (and log why) if any field is invalid. Collect valid records into a cleaned list.

Python
records = [
    {"name": "Alice", "age": "30", "email": "[email protected]"},
    {"name": "", "age": "25", "email": "[email protected]"},
    {"name": "Charlie", "age": "not_a_number", "email": "[email protected]"},
    {"name": "Diana", "age": "28", "email": "[email protected]"},
    {"name": "Eve", "age": "-5", "email": "[email protected]"},
    {"name": "Frank", "age": "45", "email": ""},
    {"name": "Grace", "age": "33", "email": "[email protected]"},
]

cleaned = []

for i, rec in enumerate(records, 1):
    # Guard 1: name must not be empty
    if not rec["name"]:
        print(f"SKIP: Record {i} — empty name")
        continue

    # Guard 2: age must be a valid integer
    try:
        age = int(rec["age"])
    except ValueError:
        print(f"SKIP: Record {i} — invalid age '{rec['age']}'")
        continue

    # Guard 3: age must be positive
    if age <= 0:
        print(f"SKIP: Record {i} — age out of range: {age}")
        continue

    # Guard 4: email must not be empty
    if not rec["email"]:
        print(f"SKIP: Record {i} — empty email")
        continue

    # All checks passed — record is valid
    cleaned.append((rec["name"], age, rec["email"]))

print(f"Cleaned records ({len(cleaned)}):")
for name, age, email in cleaned:
    print(f"  {name}, {age}, {email}")
Solution
SKIP: Record 2 — empty name
SKIP: Record 3 — invalid age 'not_a_number'
SKIP: Record 5 — age out of range: -5
SKIP: Record 6 — empty email
Cleaned records (3):
Alice, 30, [email protected]
Diana, 28, [email protected]
Grace, 33, [email protected]

This is the guard clause pattern applied to loops. Each continue acts as an early rejection — if a record fails any check, we log it and move on immediately. The "happy path" code (appending to cleaned) sits at the bottom with zero extra indentation.

Compare the alternative without continue:

for i, rec in enumerate(records, 1):
if rec["name"]:
try:
age = int(rec["age"])
if age > 0:
if rec["email"]:
cleaned.append((rec["name"], age, rec["email"]))
else:
print(f"SKIP: Record {i} — empty email")
else:
print(f"SKIP: Record {i} — age out of range")
except ValueError:
print(f"SKIP: Record {i} — invalid age")
else:
print(f"SKIP: Record {i} — empty name")

The nested version is harder to read, harder to maintain, and the error messages are far from their conditions. The continue-based version is the standard production pattern for data validation loops.

records = [
    {"name": "Alice", "age": "30", "email": "[email protected]"},
    {"name": "", "age": "25", "email": "[email protected]"},
    {"name": "Charlie", "age": "not_a_number", "email": "[email protected]"},
    {"name": "Diana", "age": "28", "email": "[email protected]"},
    {"name": "Eve", "age": "-5", "email": "[email protected]"},
    {"name": "Frank", "age": "45", "email": ""},
    {"name": "Grace", "age": "33", "email": "[email protected]"},
]

# Process records, skipping invalid ones.
# Invalid if: name is empty, age is not a positive integer, email is empty.
# Print why each record is skipped.
# For valid records, collect them in a cleaned list.

cleaned = []
# Your code here
Expected Output
SKIP: Record 2 — empty name
SKIP: Record 3 — invalid age 'not_a_number'
SKIP: Record 5 — age out of range: -5
SKIP: Record 6 — empty email
Cleaned records (3):
  Alice, 30, [email protected]
  Diana, 28, [email protected]
  Grace, 33, [email protected]
Hints

Hint 1: Use enumerate(records, 1) so record numbering starts at 1. Check each validation rule in order: empty name, non-integer age, non-positive age, empty email. Use continue after each failed check.

Hint 2: To check if age is a valid integer, use str.isdigit(). But careful — isdigit() returns False for negative numbers since the minus sign is not a digit. Convert with int() and check > 0 separately.

#6Prime Number Checker with breakMedium
breakfor-elseprime-checkmath

Implement a prime checker using break and for-else. A number is prime if no integer from 2 to sqrt(n) divides it evenly. Use for-else to distinguish "found a factor" from "no factor exists."

Python
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:
            break            # Found a factor — not prime
    else:
        return True          # No factor found — prime

    return False             # break fired — not prime


# Test with a range of values
test_values = [1, 2, 3, 4, 11, 15, 17, 25, 29, 100]
for n in test_values:
    status = "PRIME" if is_prime(n) else "not prime"
    print(f"{n:>4d} -> {status}")
Solution
def is_prime(n):
if n < 2:
return False
for divisor in range(2, int(n ** 0.5) + 1):
if n % divisor == 0:
break # divisor found — not prime
else:
return True # loop completed — no divisor — prime
return False # reached only if break fired
1 -> not prime
2 -> PRIME
3 -> PRIME
4 -> not prime
11 -> PRIME
15 -> not prime
17 -> PRIME
25 -> not prime
29 -> PRIME
100 -> not prime

Trace through is_prime(17):

n = 17, check range(2, 5) → divisors: 2, 3, 4
17 % 2 = 1 → no break
17 % 3 = 2 → no break
17 % 4 = 1 → no break
loop exhausted → else runs → return True ✓

Trace through is_prime(15):

n = 15, check range(2, 4) → divisors: 2, 3
15 % 2 = 1 → no break
15 % 3 = 0 → BREAK!
else is SKIPPED → fall through to return False ✓

Why sqrt(n)? If n has a factor greater than sqrt(n), it must also have one less than sqrt(n) (since factors come in pairs). So checking up to sqrt(n) is sufficient. For n = 100, we only check 2, 3, ..., 10 instead of 2, 3, ..., 99.

def is_prime(n):
    """Return True if n is prime, False otherwise.
    
    Use a loop with break and for-else. Handle edge cases (n < 2).
    Only check divisors up to sqrt(n) for efficiency.
    """
    # Your code here
    pass


# Test with a range of values
test_values = [1, 2, 3, 4, 11, 15, 17, 25, 29, 100]
for n in test_values:
    status = "PRIME" if is_prime(n) else "not prime"
    print(f"{n:>4d} -> {status}")
Expected Output
   1 -> not prime
   2 -> PRIME
   3 -> PRIME
   4 -> not prime
  11 -> PRIME
  15 -> not prime
  17 -> PRIME
  25 -> not prime
  29 -> PRIME
 100 -> not prime
Hints

Hint 1: Handle n < 2 as a special case first (not prime). Then loop from 2 up to int(n**0.5) + 1. If any divisor divides n evenly, break — it is not prime.

Hint 2: The for-else is the key: if you break (found a divisor), the else does not run. If the loop completes (no divisor found), the else runs — that means the number is prime.

#7Multi-Condition Filter with continueMedium
continuemulti-filterdata-processing

Filter financial transactions using continue as a multi-stage gate. Only USD, completed, non-zero, reasonably-sized transactions should pass through. Log every rejection with its reason.

Python
transactions = [
    {"id": "T001", "amount": 150.00, "currency": "USD", "status": "completed"},
    {"id": "T002", "amount": 0.00, "currency": "USD", "status": "completed"},
    {"id": "T003", "amount": 75.50, "currency": "EUR", "status": "completed"},
    {"id": "T004", "amount": 200.00, "currency": "USD", "status": "refunded"},
    {"id": "T005", "amount": 10000.00, "currency": "USD", "status": "completed"},
    {"id": "T006", "amount": 89.99, "currency": "USD", "status": "completed"},
    {"id": "T007", "amount": 45.00, "currency": "GBP", "status": "pending"},
    {"id": "T008", "amount": 320.00, "currency": "USD", "status": "completed"},
]

valid = []
total = 0.0

for txn in transactions:
    tid = txn["id"]

    # Gate 1: must be completed
    if txn["status"] != "completed":
        print(f"SKIP {tid}: status '{txn['status']}'")
        continue

    # Gate 2: must be USD
    if txn["currency"] != "USD":
        print(f"SKIP {tid}: currency {txn['currency']} (not USD)")
        continue

    # Gate 3: amount must be positive
    if txn["amount"] <= 0:
        print(f"SKIP {tid}: zero amount")
        continue

    # Gate 4: amount must not exceed limit
    if txn["amount"] > 5000:
        print(f"SKIP {tid}: amount \${txn['amount']:.2f} exceeds \$5000 limit")
        continue

    # All gates passed
    valid.append(txn)
    total += txn["amount"]

print("Valid USD transactions:")
for txn in valid:
    print(f"  {txn['id']}: \${txn['amount']:.2f}")
print(f"Total: \${total:.2f} ({len(valid)} transactions)")
Solution
SKIP T002: zero amount
SKIP T003: currency EUR (not USD)
SKIP T004: status 'refunded'
SKIP T005: amount $10000.00 exceeds $5000 limit
SKIP T007: status 'pending'
Valid USD transactions:
T001: $150.00
T006: $89.99
T008: $320.00
Total: $559.99 (3 transactions)

This is the pipeline-filter pattern: each continue is a gate that rejects bad data early. The order matters for two reasons:

  1. Performance: Check the cheapest condition first. String comparison (status) is cheaper than numeric comparison, which is cheaper than a regex or API call.
  2. Logging: The first failing condition is the one that gets logged. If you checked amount > 5000 before currency != "USD", transaction T003 would be logged as "amount too low" instead of the more informative "wrong currency."

Production extension: In real systems, you would often count rejections by reason for monitoring:

from collections import Counter
rejections = Counter()

# Inside each skip:
rejections["zero_amount"] += 1

# After the loop:
print(f"Rejection summary: {dict(rejections)}")
transactions = [
    {"id": "T001", "amount": 150.00, "currency": "USD", "status": "completed"},
    {"id": "T002", "amount": 0.00, "currency": "USD", "status": "completed"},
    {"id": "T003", "amount": 75.50, "currency": "EUR", "status": "completed"},
    {"id": "T004", "amount": 200.00, "currency": "USD", "status": "refunded"},
    {"id": "T005", "amount": 10000.00, "currency": "USD", "status": "completed"},
    {"id": "T006", "amount": 89.99, "currency": "USD", "status": "completed"},
    {"id": "T007", "amount": 45.00, "currency": "GBP", "status": "pending"},
    {"id": "T008", "amount": 320.00, "currency": "USD", "status": "completed"},
]

# Filter for valid USD transactions:
# - Must be "completed" status
# - Must be "USD" currency
# - Amount must be > 0
# - Amount must be <= 5000 (flag large transactions)
# Use continue for each rejection. Collect and summarize valid ones.

valid = []
total = 0.0
# Your code here
Expected Output
SKIP T002: zero amount
SKIP T003: currency EUR (not USD)
SKIP T004: status 'refunded'
SKIP T005: amount $10000.00 exceeds $5000 limit
SKIP T007: status 'pending'
Valid USD transactions:
  T001: $150.00
  T006: $89.99
  T008: $320.00
Total: $559.99 (3 transactions)
Hints

Hint 1: Check conditions in order of cheapest-to-evaluate first: status, currency, zero amount, amount limit. Use continue after each failed check to skip to the next transaction.

Hint 2: Accumulate both the list of valid transactions and a running total. Print the summary at the end.


Hard

#8Search a 2D Grid with Nested Loop EscapeHard
breaknested-loops2d-gridsentinel

Search a 2D grid for target values. The challenge: break only exits the inner loop. You need a strategy to escape both loops when the target is found. Print the search path so you can verify early termination.

Python
grid = [
    [1,  2,  3,  4,  5],
    [6,  7,  8,  9, 10],
    [11, 12, 13, 14, 15],
    [16, 17, 18, 19, 20],
]

targets = [14, 7, 99]

for target in targets:
    print(f"--- Searching for {target} ---")
    found = False

    for r, row in enumerate(grid):
        cells = ""
        for c, val in enumerate(row):
            cells += f"  ({r},{c})={val}"
            if val == target:
                print(f"  Checking{cells}")
                print(f"  FOUND {target} at row={r}, col={c}")
                found = True
                break          # Exit inner loop
        else:
            # Inner loop completed without break — print full row
            print(f"  Checking{cells}")
            continue           # Continue outer loop
        # Inner loop was broken — break outer loop too
        break

    if not found:
        print(f"  {target} NOT FOUND in grid")
Solution
--- Searching for 14 ---
Checking (0,0)=1 (0,1)=2 (0,2)=3 (0,3)=4 (0,4)=5
Checking (1,0)=6 (1,1)=7 (1,2)=8 (1,3)=9 (1,4)=10
Checking (2,0)=11 (2,1)=12 (2,2)=13 (2,3)=14
FOUND 14 at row=2, col=3
--- Searching for 7 ---
Checking (0,0)=1 (0,1)=2 (0,2)=3 (0,3)=4 (0,4)=5
Checking (1,0)=6 (1,1)=7
FOUND 7 at row=1, col=1
--- Searching for 99 ---
Checking (0,0)=1 (0,1)=2 (0,2)=3 (0,3)=4 (0,4)=5
Checking (1,0)=6 (1,1)=7 (1,2)=8 (1,3)=9 (1,4)=10
Checking (2,0)=11 (2,1)=12 (2,2)=13 (2,3)=14 (2,4)=15
Checking (3,0)=16 (3,1)=17 (3,2)=18 (3,3)=19 (3,4)=20
99 NOT FOUND in grid

The nested-loop escape is a classic Python challenge. There are three common strategies:

Strategy 1 — Flag variable (used above with for-else + continue):

for row in grid:
for val in row:
if found:
break # inner break
else:
continue # inner loop finished naturally → continue outer
break # inner loop broke → break outer too

The for-else + continue + break pattern is idiomatic Python. The else clause on the inner loop runs only when the inner loop did NOT break. The continue skips the outer break. If the inner loop DID break, else is skipped, continue is skipped, and the outer break fires.

Strategy 2 — Extract to a function (cleanest):

def find_in_grid(grid, target):
for r, row in enumerate(grid):
for c, val in enumerate(row):
if val == target:
return (r, c)
return None

Strategy 3 — Exception (rare, but valid for deeply nested loops):

class Found(Exception):
pass

try:
for r, row in enumerate(grid):
for c, val in enumerate(row):
if val == target:
raise Found(f"{r},{c}")
except Found as e:
print(f"Found at {e}")

In production, prefer Strategy 2. The function-based approach is readable, testable, and avoids the cognitive overhead of tracking flag variables.

grid = [
    [1,  2,  3,  4,  5],
    [6,  7,  8,  9, 10],
    [11, 12, 13, 14, 15],
    [16, 17, 18, 19, 20],
]

targets = [14, 7, 99]

# For each target, search the grid and report its (row, col) position.
# Use nested loops with break. Remember: break only exits the INNER loop.
# You need a strategy to also exit the outer loop.
# Print each cell checked so the search path is visible.

# Your code here
Expected Output
--- Searching for 14 ---
  Checking (0,0)=1  (0,1)=2  (0,2)=3  (0,3)=4  (0,4)=5
  Checking (1,0)=6  (1,1)=7  (1,2)=8  (1,3)=9  (1,4)=10
  Checking (2,0)=11  (2,1)=12  (2,2)=13  (2,3)=14
  FOUND 14 at row=2, col=3
--- Searching for 7 ---
  Checking (0,0)=1  (0,1)=2  (0,2)=3  (0,3)=4  (0,4)=5
  Checking (1,0)=6  (1,1)=7
  FOUND 7 at row=1, col=1
--- Searching for 99 ---
  Checking (0,0)=1  (0,1)=2  (0,2)=3  (0,3)=4  (0,4)=5
  Checking (1,0)=6  (1,1)=7  (1,2)=8  (1,3)=9  (1,4)=10
  Checking (2,0)=11  (2,1)=12  (2,2)=13  (2,3)=14  (2,4)=15
  Checking (3,0)=16  (3,1)=17  (3,2)=18  (3,3)=19  (3,4)=20
  99 NOT FOUND in grid
Hints

Hint 1: break only exits the innermost loop. To escape nested loops, use a flag variable (found = True) and check it in the outer loop, or use for-else on the outer loop.

Hint 2: A cleaner approach: use for-else on BOTH loops. The inner for-else breaks when found. The outer loop checks if the inner loop broke (via a flag) and breaks too. Or wrap the whole thing in a function and use return.

#9Retry with Fallback Using for-elseHard
for-elseretry-patternerror-handlingsimulation

Build a retry-with-fallback system using for-else. Simulate an unreliable service — retry up to N times, and if all attempts fail, the else clause activates a fallback. This is one of the most practical uses of for-else in production Python.

Python
def simulate_service(fail_sequence):
    """Returns a callable that fails according to the sequence, then succeeds."""
    calls = iter(fail_sequence)

    def call():
        error = next(calls, None)
        if error is not None:
            raise ConnectionError(error)
        return "{data: ok}"

    return call


def call_with_retry(service, max_retries, fallback_result):
    """Try calling service up to max_retries times. Fall back if all fail."""
    result = None

    for attempt in range(1, max_retries + 1):
        try:
            result = service()
            print(f"  Attempt {attempt}/{max_retries}: SUCCESS — got result: {result}")
            break
        except ConnectionError as e:
            print(f"  Attempt {attempt}/{max_retries}: FAILED ({e})")
    else:
        # All retries exhausted — no break ever fired
        print(f"  FALLBACK: All {max_retries} attempts failed. Using cached response.")
        result = fallback_result

    return result


# Scenario 1: fails twice, then succeeds
print("=== Scenario 1: Service recovers on attempt 3 ===")
service1 = simulate_service(["ServiceTimeout", "ConnectionReset"])
r1 = call_with_retry(service1, max_retries=5, fallback_result="{data: cached_fallback}")
print(f"  Total attempts: 3")

# Scenario 2: fails every time
print("=== Scenario 2: Service never recovers ===")
service2 = simulate_service(["ServiceTimeout", "ConnectionReset", "ServiceTimeout"])
r2 = call_with_retry(service2, max_retries=3, fallback_result="{data: cached_fallback}")
print(f"  Result: {r2}")
Solution
=== Scenario 1: Service recovers on attempt 3 ===
Attempt 1/5: FAILED (ServiceTimeout)
Attempt 2/5: FAILED (ConnectionReset)
Attempt 3/5: SUCCESS — got result: {data: ok}
Total attempts: 3
=== Scenario 2: Service never recovers ===
Attempt 1/3: FAILED (ServiceTimeout)
Attempt 2/3: FAILED (ConnectionReset)
Attempt 3/3: FAILED (ServiceTimeout)
FALLBACK: All 3 attempts failed. Using cached response.
Result: {data: cached_fallback}

This is the canonical production use of for-else. The pattern maps perfectly:

for attempt in range(max_retries):
try:
result = risky_call()
break # Success — stop retrying
except TransientError:
log_and_wait() # Failed — try again
else:
activate_fallback() # All retries exhausted

Without for-else, you need a flag:

succeeded = False
for attempt in range(max_retries):
try:
result = risky_call()
succeeded = True
break
except TransientError:
pass

if not succeeded:
activate_fallback()

The for-else version eliminates the flag variable entirely. The intent is clearer: "try these attempts, and if none worked, fall back."

Production enhancements you would add:

  1. Exponential backoff: time.sleep(2 ** attempt) between retries
  2. Jitter: Add random delay to prevent thundering herd
  3. Retry budget: Track retries across calls, not just per call
  4. Circuit breaker: After N consecutive failures, stop trying for a cooldown period
import random

# Simulate an unreliable service that fails randomly.
# Implement a retry loop that:
# 1. Tries up to max_retries times
# 2. If a try succeeds, break and report success
# 3. If ALL tries fail, the else clause activates the fallback
# 4. Track and display each attempt's outcome

# Your code here
Expected Output
=== Scenario 1: Service recovers on attempt 3 ===
  Attempt 1/5: FAILED (ServiceTimeout)
  Attempt 2/5: FAILED (ConnectionReset)
  Attempt 3/5: SUCCESS — got result: {data: ok}
  Total attempts: 3
=== Scenario 2: Service never recovers ===
  Attempt 1/3: FAILED (ServiceTimeout)
  Attempt 2/3: FAILED (ConnectionReset)
  Attempt 3/3: FAILED (ServiceTimeout)
  FALLBACK: All 3 attempts failed. Using cached response.
  Result: {data: cached_fallback}
Hints

Hint 1: Use a for loop over range(max_retries). Inside the loop, simulate the call. If it succeeds, store the result and break. If it fails, print the error and continue to the next attempt.

Hint 2: The else clause on the for loop is the fallback — it runs only when ALL retries are exhausted (break never fired). This is the perfect use case for for-else in production code.

#10Simple Tokenizer with break and continueHard
breakcontinueparsingtokenizerstate-machine

Build a simple tokenizer that breaks an expression string into tokens. Use continue to skip whitespace, inner loops to consume multi-character tokens (numbers, identifiers), and break to halt on invalid characters. This mirrors how real parsers and lexers work.

Python
def tokenize(text):
    """Tokenize a simple expression into (type, value) pairs."""
    tokens = []
    pos = 0
    operators = set("+-*/=")
    error = None

    while pos < len(text):
        char = text[pos]

        # Skip whitespace
        if char == " ":
            pos += 1
            continue

        # Operators — single character tokens
        if char in operators:
            tokens.append(("OP", char))
            pos += 1
            continue

        # Numbers — consume all consecutive digits
        if char.isdigit():
            start = pos
            while pos < len(text) and text[pos].isdigit():
                pos += 1
            tokens.append(("NUMBER", text[start:pos]))
            continue

        # Identifiers — consume all consecutive letters
        if char.isalpha():
            start = pos
            while pos < len(text) and text[pos].isalpha():
                pos += 1
            tokens.append(("IDENT", text[start:pos]))
            continue

        # Unknown character — error, stop tokenizing
        error = f"unexpected character '{char}' at position {pos}"
        break

    return tokens, error


# Test 1: valid expression
expr1 = "x = 10 + y * 3 - 2"
tokens1, err1 = tokenize(expr1)
print(f"Input: '{expr1}'")
print("Tokens:")
for ttype, tval in tokens1:
    print(f"  {ttype:<7s} '{tval}'")
print(f"Total: {len(tokens1)} tokens")

# Test 2: identifiers with multiple characters
print(f"\nInput: 'result = alpha + 42'")
tokens2, err2 = tokenize("result = alpha + 42")
print("Tokens:")
for ttype, tval in tokens2:
    print(f"  {ttype:<7s} '{tval}'")
print(f"Total: {len(tokens2)} tokens")

# Test 3: invalid character causes break
expr3 = "a + b @ c"
tokens3, err3 = tokenize(expr3)
print(f"\nInput: '{expr3}'")
print("Tokens:")
for ttype, tval in tokens3:
    print(f"  {ttype:<7s} '{tval}'")
if err3:
    print(f"  ERROR: {err3}")
print(f"Total: {len(tokens3)} tokens (stopped at error)")
Solution
Input: 'x = 10 + y * 3 - 2'
Tokens:
IDENT 'x'
OP '='
NUMBER '10'
OP '+'
IDENT 'y'
OP '*'
NUMBER '3'
OP '-'
NUMBER '2'
Total: 9 tokens

Input: 'result = alpha + 42'
Tokens:
IDENT 'result'
OP '='
IDENT 'alpha'
OP '+'
NUMBER '42'
Total: 5 tokens

Input: 'a + b @ c'
Tokens:
IDENT 'a'
OP '+'
IDENT 'b'
ERROR: unexpected character '@' at position 6
Total: 3 tokens (stopped at error)

This tokenizer demonstrates all three control flow tools working together:

ToolRole in the tokenizer
continueSkip whitespace — advance pos and jump back to the top of the while loop
continue (after consuming)After building a multi-char token (number or identifier), jump back without incrementing pos again (the inner while already advanced it)
breakStop on invalid input — we cannot proceed, so we exit the main loop entirely
Inner while loops"Consume" pattern — keep advancing pos as long as characters match the current token type

How real tokenizers/lexers extend this pattern:

  1. String literals: When you see ", consume until the next " (handling escape sequences like \")
  2. Comments: When you see #, consume until end-of-line and continue (skip the comment)
  3. Multi-character operators: When you see =, peek ahead — if next is =, emit == (comparison) instead of = (assignment)
  4. Error recovery: Instead of break on error, you could skip the bad character and continue — this lets you report multiple errors in one pass

The key design principle: Each iteration of the main while loop handles exactly one token (or skips whitespace). The continue statements ensure the loop restarts cleanly after each token, and break provides a clean emergency exit.

# Build a simple expression tokenizer that processes a string character
# by character, using break and continue to handle different token types.
#
# Token types: NUMBER, IDENT (identifier), OP (operator), SKIP (whitespace)
# Operators: +, -, *, /, =
# Identifiers: sequences of letters
# Numbers: sequences of digits (integers only)
#
# Use continue to skip whitespace.
# Use break to stop at end-of-input or on an invalid character.

# Your code here
Expected Output
Input: 'x = 10 + y * 3 - 2'
Tokens:
  IDENT   'x'
  OP      '='
  NUMBER  '10'
  OP      '+'
  IDENT   'y'
  OP      '*'
  NUMBER  '3'
  OP      '-'
  NUMBER  '2'
Total: 9 tokens

Input: 'result = alpha + 42'
Tokens:
  IDENT   'result'
  OP      '='
  IDENT   'alpha'
  OP      '+'
  NUMBER  '42'
Total: 5 tokens

Input: 'a + b @ c'
Tokens:
  IDENT   'a'
  OP      '+'
  IDENT   'b'
  ERROR: unexpected character '@' at position 6
Total: 3 tokens (stopped at error)
Hints

Hint 1: Use a while loop with a position index (pos). Inside the loop: check if current char is whitespace (continue), operator (emit and advance), digit (inner loop to consume all digits, then emit), letter (inner loop to consume all letters, then emit), or unknown (emit error and break).

Hint 2: The inner loops for numbers and identifiers use a while loop that advances pos as long as the next character is of the same type. This is the "consume" pattern common in parsers.

© 2026 EngineersOfAI. All rights reserved.