Loops & Iteration¶
What this lesson gives you
for over collections, while for conditions, and the Pythonic habits (enumerate, zip) that distinguish a fluent writer from a translated-from-Java one.
Estimated time: 22 min read · Part: Making Decisions: Control Flow
Learning objectives
- Iterate over any iterable using the Pythonic
for item in collection:pattern. - Use
enumerate,zip,reversed, andsortedto iterate with additional context without index juggling. - Write
whileloops that always make progress toward their exit condition. - Use
break,continue, and thefor/elsepattern for search loops. - Use key functions from
itertoolsto handle common patterns concisely.
Python's for loop iterates directly over the items of a collection — it doesn't count indices like C. This "for each" model is the single biggest readability win over older languages, and writing index-juggling loops is the clearest sign someone is translating from another language rather than thinking in Python. The underlying mechanism is the iterator protocol (covered in depth in Lesson 4.2), which means anything that implements __iter__ works in a for loop — lists, tuples, strings, dicts, files, generators, database cursors, and custom objects alike.
clients = ["Acme", "Globex", "Initech"]
# ── Basic iteration: items directly, no index needed ──
for client in clients:
print(client.upper())
# ── enumerate: index + item in one step ──
for i, client in enumerate(clients, start=1):
print(f"{i}. {client}") # 1. Acme / 2. Globex / 3. Initech
# ── zip: walk two sequences in lockstep ──
mrrs = [9900, 14500, 2100]
for client, mrr in zip(clients, mrrs):
print(f"{client}: ${mrr:,}")
# ── zip with unequal lengths (zip_longest for full coverage) ──
from itertools import zip_longest
for c, m in zip_longest(clients, [100, 200], fillvalue=0):
print(c, m) # Initech gets fillvalue=0
ACME GLOBEX INITECH 1. Acme 2. Globex 3. Initech Acme: $9,900 Globex: $14,500 Initech: $2,100
The Java/C habit to drop
Anti-pattern: for i in range(len(clients)): print(clients[i]). It works but is noisy and un-Pythonic. Iterate the collection directly; use enumerate when you also need the position. Reviewers (and interviewers) read range(len(...)) as a signal that you haven't internalised Python's iteration model. The only legitimate case for range(len(...)) is when you need to iterate while modifying a list by index — and even then, iterating a copy is usually cleaner.
Iterating dicts, sorted, and reversed¶
Dicts are iterable in Python 3.7+ (insertion-ordered). By default, iterating a dict yields its keys. The .items() method yields key-value pairs, .values() yields values, and .keys() yields keys explicitly. All three are lazy view objects, not lists — they reflect the current dict state dynamically.
deal_mrrs = {"Acme": 9900, "Globex": 14500, "Initech": 2100}
# Iterate keys:
for name in deal_mrrs:
print(name)
# Iterate key-value pairs — most common:
for name, mrr in deal_mrrs.items():
print(f"{name}: ${mrr:,}")
# Sort by value descending:
for name, mrr in sorted(deal_mrrs.items(), key=lambda kv: kv[1], reverse=True):
print(f"{name}: ${mrr:,}")
# Reverse a list without modifying it:
for client in reversed(list(deal_mrrs.keys())):
print(client)
while, break, continue, and for/else¶
Use while when you loop until a condition changes rather than over a known collection. break exits the innermost loop immediately; continue skips the rest of the current iteration and moves to the next. Python also has a little-known for/else (and while/else) construct: the else block runs only if the loop completed without hitting a break — elegant for search patterns.
# while loop with break:
attempts = 0
while attempts < 3:
pwd = input("password: ")
if pwd == "secret":
print("welcome")
break
attempts += 1
else:
print("locked out") # runs only if while exhausted without break
# for/else — elegant search pattern:
targets = ["Acme", "Globex", "Initech"]
for target in targets:
if target.startswith("G"):
print(f"Found: {target}")
break
else:
print("No match found") # only if no break was hit
# continue: skip current iteration
for n in range(10):
if n % 2 == 0:
continue # skip even numbers
print(n) # 1, 3, 5, 7, 9
Found: Globex 1 3 5 7 9
Beware the infinite loop
A while condition that never becomes false runs forever (Ctrl-C to stop). Always ensure something inside the loop moves toward the exit — increment a counter, consume input, or break. The classic bug is forgetting the increment (while n < 10: without n += 1). Infinite loops are occasionally intentional in event loops and servers — in that case, make the intent explicit with while True: and a clear break condition.
itertools: the loops you didn't know you needed¶
The itertools module provides lazy, memory-efficient building blocks for iteration. These are not obscure — they appear constantly in data pipelines, combinatorial problems, and ETL code. Knowing the key ones signals Python fluency beyond beginner level.
import itertools
# chain: iterate multiple iterables as one
q1 = ["Jan", "Feb", "Mar"]
q2 = ["Apr", "May", "Jun"]
for month in itertools.chain(q1, q2):
print(month, end=" ") # Jan Feb Mar Apr May Jun
# islice: take the first N items from any iterable (no list needed)
import itertools
def naturals():
n = 1
while True:
yield n; n += 1
print(list(itertools.islice(naturals(), 5))) # [1, 2, 3, 4, 5]
# groupby: group consecutive elements by a key
events = [
{"region": "EMEA", "deal": "Acme"},
{"region": "EMEA", "deal": "Globex"},
{"region": "AMER", "deal": "Initech"},
]
events_sorted = sorted(events, key=lambda e: e["region"])
for region, group in itertools.groupby(events_sorted, key=lambda e: e["region"]):
deals = [e["deal"] for e in group]
print(f"{region}: {deals}")
# AMER: ['Initech']
# EMEA: ['Acme', 'Globex']
Jan Feb Mar Apr May Jun [1, 2, 3, 4, 5] AMER: ['Initech'] EMEA: ['Acme', 'Globex']
PYTHON LOOP CHOICE GUIDE
═══════════════════════════════════════════════════════
Need to process each item?
└─► for item in collection
Need item + position?
└─► for i, item in enumerate(collection)
Need to walk two lists together?
└─► for a, b in zip(list1, list2)
Need to loop until a condition?
└─► while condition: ...
Need all combinations?
└─► itertools.product(a, b)
Need to group by a key?
└─► itertools.groupby(sorted_data, key=...)
Need the first N of an infinite sequence?
└─► itertools.islice(gen, N)
Need to concatenate iterables lazily?
└─► itertools.chain(iter1, iter2, ...)
✗ AVOID: for i in range(len(seq)): seq[i]
| Pattern | Use when | Example |
|---|---|---|
for x in seq | You need each element | Process all records |
enumerate(seq, start=1) | Need position + element | Numbered report rows |
zip(a, b) | Walk two sequences together | Pair headers with values |
zip_longest(a, b, fillvalue=X) | Walk unequal sequences | Merge with defaults |
sorted(seq, key=fn) | Iterate in sorted order without mutating | Rank by MRR |
reversed(seq) | Iterate backward without mutating | Display history newest-first |
itertools.chain(*iters) | Concatenate iterables lazily | Process Q1+Q2 data together |
itertools.groupby(seq, key) | Group consecutive items (requires pre-sort) | Group events by region |
itertools.islice(gen, n) | Take first N from infinite/large stream | Paginate a database cursor |
itertools.product(a, b) | All combinations (Cartesian product) | Test all parameter combinations |
WHY iterate items, not indices Index-based iteration requires the programmer to manage an extra variable, bounds-check it, and use it to look up the actual item. Direct item iteration eliminates all three steps. It also works on any iterable, not just indexable sequences — you can iterate a file, a network stream, or a database cursor with the same for loop syntax, no length needed.
HOW for/else is useful in practice The search pattern is the clearest use: you loop looking for something, break when found, and the else-block handles the "not found" case. Without for/else, you'd need a boolean flag: found = False; for...; if not found: .... The for/else version has one fewer variable and makes the intent explicit. It's unusual enough that a comment (# else: not found) is welcome.
WHERE itertools solves real consulting problems In data pipelines: chain merges multiple data sources without building intermediate lists. groupby aggregates before writing to a report. islice pages through a large cursor. These patterns appear every time you process CSV exports, API responses, or database results — exactly the work that appears in solutions consulting scripts.
Knowledge check
InterviewYou need to print each client alongside its 1-based rank. Which is the cleanest, most idiomatic Python?
for i in range(len(clients)): print(i+1, clients[i])for rank, client in enumerate(clients, start=1): print(rank, client)i = 0\nwhile i < len(clients): print(i+1, clients[i]); i += 1- You can't get an index in a Python
forloop.
Answer
for rank, client in enumerate(clients, start=1): print(rank, client)
enumerate(clients, start=1) yields (rank, client) pairs directly and starts counting at 1 — the idiomatic solution. The range(len(...)) and manual while versions work but are exactly the un-Pythonic patterns interviewers notice.
Knowledge check
Concept checkWhen does the else clause of a for loop execute?
- When the loop body raises an exception.
- Always, after the loop finishes.
- Only when the loop runs to completion without hitting a
breakstatement — it is skipped ifbreakexits the loop early. - When the iterable was empty.
Answer
Only when the loop runs to completion without hitting a break statement — it is skipped if break exits the loop early.
The for/else pattern is specifically designed for search loops: loop looking for something, break when found, and the else-block handles the not-found case. If the loop runs all the way through without a break, else fires. If break exits early, else is skipped. This eliminates the boolean "found" flag pattern.
Knowledge check
AppliedYou have two lists of equal length: client names and their MRR values. You want to create a dict mapping name to MRR. What's the most Pythonic one-liner?
result = {}; for i in range(len(names)): result[names[i]] = mrrs[i]result = dict(zip(names, mrrs))— zip pairs them, dict() converts the pairs to a mapping.- You need to use enumerate to do this.
result = {names: mrrs}
Answer
result = dict(zip(names, mrrs)) — zip pairs them, dict() converts the pairs to a mapping.
dict(zip(names, mrrs)) is the idiomatic Python one-liner. zip produces (name, mrr) pairs, and dict() turns those pairs into a dictionary. This is a very common pattern in data manipulation and appears constantly in real code.
Exercise
Write a function batch_process(records: list[dict], batch_size: int) -> list[list[dict]] that splits a list of records into batches of at most batch_size each. Use range() and list slicing. Then write a second version using itertools.batched (Python 3.12+) or itertools.islice. Print each batch's size to confirm correctness with 7 records in batches of 3.
Show solution
def batch_process(records: list[dict], batch_size: int) -> list[list[dict]]:
return [
records[i:i + batch_size]
for i in range(0, len(records), batch_size)
]
# Using itertools.batched (Python 3.12+):
import itertools
def batch_process_v2(records: list[dict], batch_size: int) -> list[list[dict]]:
return [list(batch) for batch in itertools.batched(records, batch_size)]
# Test:
records = [{"id": i, "name": f"Deal-{i}"} for i in range(1, 8)]
for batch in batch_process(records, 3):
print(f"Batch of {len(batch)}: {[r['id'] for r in batch]}")
# Batch of 3: [1, 2, 3]
# Batch of 3: [4, 5, 6]
# Batch of 1: [7]
Key takeaways
for item in collection:iterates items directly — the readable, idiomatic model.- Use
enumeratefor index+item andzipto walk multiple sequences — notrange(len(...)). whileloops until a condition flips; always ensure progress toward the exit condition.breakexits early;continueskips to the next iteration;for/elsehandles the "not found" case.itertoolsprovides lazy, composable tools for common patterns:chain,islice,groupby,product.