03. Control Flow - Python
Quote
“The quality of programmers is a decreasing function of the density of go to statements in the programs they produce.”
— Edsger W. Dijkstra, Go To Statement Considered Harmful (1968)
Summary
Conditional Statements
if/elif/elseuses indentation-based blocks; conditions need no parentheses; first matching branch wins- Ternary:
value_if_true if condition else value_if_false— reverse order vs C#; avoid nesting beyond 2 levels- Truthy/falsy:
0,"",None,[],{}are falsy; non-empty collections and non-zero numbers are truthy- Chained comparisons:
10 < x < 20evaluatesxonce — no equivalent in C#match/case(Python 3.10+): structural pattern matching with destructuring, type checks, guard clauses (if), OR patterns (|), and wildcard_; bare variable names incasecapture rather than compareLoops
foriterates any iterable (list, range, dict, generator, string, file) — no C-stylefor(;;)range(start, stop, step): lazy, stop is exclusive;500 in range(1000)is O(1)enumerate(iterable, start=0)yields(index, value)tuples — replacesrange(len(items))zip(a, b)stops at the shortest iterable; useitertools.zip_longestfor unequal lengthswhilerepeats while condition is truthy; nodo-while— usewhile True: ... if cond: breakfor...else:elseblock runs only when nobreakoccurred — clean “not found” idiomLoop Control
breakexits the innermost loop only — no labeled break in Pythoncontinueskips to the next iteration of the innermost looppassis a no-op placeholder required where syntax demands a block; temporary scaffolding only- Walrus operator
:=assigns and returns in one expression — eliminates read-before-loop duplication; works inwhileconditions and comprehension filters- Multi-level exit: use a flag variable +
break, or extract to a function andreturnIterators & Generators
- Iterator protocol:
__iter__()+__next__(); raisesStopIterationwhen exhausted; single-passyieldturns a function into a generator; execution suspends at eachyieldand resumes onnext()yield from iterabledelegates to a sub-generator — replacesfor item in sub: yield item- Generator expression:
(expr for x in iter if cond)— lazy, constant memory; omit outer parens as function arg- Memory: list of 100k items ≈ 800 KB; equivalent generator object ≈ 192 bytes
- Infinite generators use
while True+yield; always limit withisliceorbreak— never calllist()on themitertools:chain,cycle,repeat,accumulate,product— all lazy- Flatten strategies:
chain.from_iterable(1 level),more_itertools.collapse(any depth), stack-based iterative (no deps),pd.json_normalize(nested dicts)Comprehensions & Functional Tools
- List:
[expr for x in iter if cond]— optimized at bytecode level, faster thanfor+append- Dict:
{k: v for ...}; Set:{expr for ...}— auto-deduplicates- Nested comprehension: outer loop first, inner second; max 2 levels
map(func, iter)andfilter(pred, iter)are lazy; prefer comprehensions with lambdas for readabilityreduce(func, iter, init)for custom folds; prefer built-inssum,max,min,any,allfor common reductionssorted(iter, key=func, reverse=bool)returns a new list; Python sort is stable; multi-key: return tuple fromkeyOperations & Safety
- Modifying a collection during iteration causes skipped items or
RuntimeError— iterate a copy or use comprehension- Generators are single-pass — re-call the function to get a fresh iterator
passin productionexceptblocks silently swallows errors — always log at minimummatch/casedoes not enforce exhaustiveness at compile time — missing cases fail silently; always addcase _:- Python loops are materially slower than C# in CPU-bound paths — use NumPy/Polars vectorization for tight loops
Glossary
if/elif/else
Conditional branching construct; Python uses indentation (not braces) to delimit blocks;
elifreplaces C#‘selse if; conditions need no parentheses; first matching branch winsDirects program execution based on boolean conditions; the primary tool for multi-tier conditional logic
Flatten with early return. Replace
if cond: do_work() else: returnwithif not cond: return; do_work()to reduce nesting levels.
Ternary expression
Inline conditional:
value_if_true if condition else value_if_false; reversed order vs C#‘scondition ? true : false; can be chained but readability degrades past 2 levelsEnables single-line conditional assignment without a full
if/elseblockReversed operand order vs C#. Python puts the true-branch first:
x if cond else y. C# puts the condition first:cond ? x : y. The inversion causes subtle bugs when reading Python with C# muscle memory.
match/case
Structural pattern matching introduced in Python 3.10; matches value, type, sequence, mapping, and nested patterns; supports OR patterns (
|), guard clauses (if), and variable binding;_is the wildcard catch-allReplaces long
if/elifchains for multi-branch dispatch and type narrowing; reduces boilerplate for heterogeneous data structuresBare variable names capture, not compare. In
case cmd:,cmdcaptures the matched value — it does not compare against an existing variable namedcmd. Use a literal (case "start":) or a guard (case c if c == expected:) for equality tests.
forloop
Iterates over any object implementing the iterator protocol (
__iter__/__next__): lists, tuples, strings, dicts, ranges, generators, files; no index-based C-stylefor(;;)Primary construct for processing every element in a collection; the backbone of ETL record-by-record processing
Never modify the iterable during iteration. Adding or removing items from a list inside the loop causes skipped elements or
RuntimeErrorfor dicts and sets. Iterate overitems.copy()or build a new collection with a comprehension.
whileloop
Repeats the body while the condition is truthy; body may never execute if condition is falsy from the start; combine with
:=(walrus) for read-and-test patterns; nodo-while— usewhile True: ... if cond: breakUsed when the number of iterations is not known in advance: polling, retry logic, input validation
Condition must become falsy. Forgetting to update the loop variable creates an infinite loop. Always ensure the condition progresses toward falsy, or include an explicit
breakwith a timeout counter.
range()
Lazy integer sequence generator:
range(start, stop, step);stopis exclusive; negative step for countdown; membership testn in range(...)is O(1)Generates index sequences for counted loops, replaces C-style
for(i=0; i<n; i++)Stop is exclusive — remember the off-by-one.
range(5)produces 0, 1, 2, 3, 4. To include 5, writerange(6)orrange(1, 6).
enumerate()
Wraps an iterable to yield
(index, value)tuples; optionalstartparameter shifts the index base; lazyReplaces manual counter variables and
range(len(items))patterns; cleaner and immune to off-by-one errorsAlways unpack the tuple. Use
for i, val in enumerate(items):— notfor pair in enumerate(items): pair[0]. Destructuring keeps the code readable and avoids tuple indexing noise.
zip()
Iterates over multiple iterables in parallel, yielding tuples of corresponding elements; stops at the shortest iterable; lazy; use
itertools.zip_longest(fillvalue=None)to pad unequal lengthsPairs corresponding elements from parallel sequences without index arithmetic
Silent truncation on unequal lengths.
zip([1,2,3], [10,20])silently drops the3. If equal-length input is not guaranteed, useitertools.zip_longestor assert lengths match before zipping.
break
Exits the innermost enclosing loop immediately; does not affect outer loops; the
for...elseelseblock is skipped whenbreakfires; Python has no labeled breakStops early when a search condition is met or a timeout is reached, avoiding unnecessary iterations
Multi-level exit via function return. Wrap nested loops in a function and use
returnto exit all levels at once — cleaner than flag variables.
continue
Skips the remainder of the current loop body and jumps to the next iteration of the innermost loop
Filters specific items inline without restructuring the loop body with nested
if/elseblocksPrefer filtering the iterable. When skipping many items, replace
if cond: continuewith a filtered comprehension orfilter()before the loop — the intent is clearer.
pass
No-op statement; required where Python syntax expects an indented block but no action is needed: empty functions, classes,
exceptblocks, and loop stubs during developmentAllows syntactically complete but intentionally empty blocks — temporary scaffolding, not permanent code
Never use
passin productionexceptblocks.except Exception: passsilently swallows all errors. At minimum log:except Exception as e: logger.warning("Unhandled: %s", e).
for...else
The
elseblock appended to afororwhileloop runs only if the loop completed without abreak; the name is counterintuitive — think of it as “no break occurred”Clean idiom for “search completed without finding a match”; eliminates a separate boolean flag variable
elsedoes not mean the loop body was falsy.for...elseis not about the truth value of the loop body. Theelseblock runs on normal completion — suppress it only withbreak.
Iterator protocol
An object is an iterator if it implements
__iter__(self)returningselfand__next__(self)raisingStopIterationwhen exhausted; iterators are single-pass — they cannot be rewoundMakes any class usable in
forloops,list(),zip(), and all iteration contexts; enables custom lazy traversal logicDistinguish iterables from iterators. A list is iterable (has
__iter__) but not an iterator (no__next__). Callingiter(my_list)returns a fresh list iterator. A generator is both — it is its own iterator.
Generator /
yield
A function containing
yieldbecomes a generator function; calling it returns a generator iterator without executing any body code; eachnext()call resumes execution until the nextyield; state is preserved between calls;StopIterationsignals exhaustion; single-passEnables memory-efficient lazy pipelines for large or infinite sequences — only one value is held in memory at a time
Calling a generator function returns the object, not the first value.
gen = countdown(5)does not start execution. The body runs only whennext(gen)or aforloop consumes it.
yield from
Delegates iteration to a sub-generator or iterable in a single expression; equivalent to
for item in sub: yield itembut also passes.send()/.throw()calls through; Python’s equivalent of C#‘sforeach (var x in sub) yield return xComposes generators and enables recursive traversal of nested structures without manual forwarding loops
yield fromon a string yields individual characters.yield from "hello"yields'h','e','l','l','o'— not the string itself. Guard withisinstance(item, str)in recursive flatten functions.
Generator expression
(expr for x in iterable if condition)— lazy comprehension producing values on demand; as a function argument, outer parentheses may be omitted:sum(x**2 for x in range(n)); single-passMemory-efficient alternative to list comprehension when the full collection is not needed — eliminates upfront allocation
Use generators when you only need to iterate once. If you need indexing,
len(), or multiple passes, use a list. If you only need to iterate once (e.g.,sum,max, pipeline), a generator saves memory with no speed penalty for large inputs.
List comprehension
[expr for x in iterable if condition]— creates a new list by transforming and optionally filtering; optimized at bytecode level; faster than equivalentfor+append; supports nesting (outer loop first)Concise, readable collection construction; the idiomatic replacement for
for+appendpatternsTwo-level nesting maximum. Beyond 2
forclauses the comprehension becomes harder to read than explicit loops. Extract inner logic into a named function.
Dict / Set comprehension
Dict:
{k: v for item in iterable if cond}— builds a dict declaratively; Set:{expr for item in iterable}— builds a deduplicated set; both supportiffiltering; colon distinguishes dict from setDeclarative construction of dicts and sets; replaces
for+d[k] = vandfor+s.add(x)patternsDuplicate keys silently overwrite. In a dict comprehension, if two iterations produce the same key, the last value wins without any error. Ensure keys are unique or handle collisions explicitly.
Walrus operator (
:=)
Assignment expression:
(var := expr)assigns the value and returns it in the same expression; valid inwhileconditions,ifconditions, and comprehension filters; introduced in Python 3.8Eliminates the “compute before the loop, test inside the loop” duplication common in
whilepolling patternsUse sparingly — simple assignment is clearer. Walrus is most valuable in
while (line := f.readline()):and comprehension filter reuse:[y for x in data if (y := f(x)) > 0]. Avoid it in straightforwardifstatements where a regular assignment reads more clearly.
itertools
Standard-library module providing lazy iterator building blocks:
chain(join iterables end-to-end),cycle(infinite repetition),repeat(same value n times),accumulate(running totals),product(Cartesian product),islice(take first n from any iterator),zip_longest(zip with padding)Composable, memory-efficient primitives for building data pipelines without materializing intermediate collections
Never call
list()on infinite itertools iterators.list(cycle([1,2,3]))hangs indefinitely. Always bound infinite iterators withisliceor abreakcondition.
Shared example setup
Examples run top to bottom. Later snippets may reuse imports, helper functions, or sample data defined earlier in the note.
The collapse() example requires more-itertools, and the json_normalize() example requires pandas. All other snippets use the standard library.
Shared imports for examples that rely on standard-library helpers.
from functools import reduce
from itertools import accumulate, chain, islice, product, repeat
import io
import sys Conditional Statements
Python provides two families of conditional constructs: if/elif/else chains with full truthy/falsy support and chained comparisons, and match/case (3.10+) for structural pattern matching with destructuring, type checking, and guards.
Decision flow for choosing between if, ternary expressions, and match.
flowchart TD A["How many branches?"] --> B["1-2 branches"] A --> C["3+ values or patterns"] B --> D["if / elif / else"] B --> E["Ternary expression"] C --> F{"Structural<br>pattern matching?"} F -->|Yes| G["match / case"] F -->|No| D D --> H["Use for side effects,<br>multiple statements"] E --> I["Use for inline<br>value selection"] G --> J["Destructuring, type checks,<br>guards, OR patterns"]
Branching with if / elif / else
Basic conditional branching uses indentation-based blocks. Python supports truthy/falsy coercion, chained comparisons (10 < x < 20), and inline ternary expressions.
if / elif / else — indentation-based branching
Python uses indentation (not braces) to define blocks. Conditions don’t need parentheses. elif chains for multiple tiers — one keyword instead of else if, fewer nesting levels.
Conditional chains check conditions top-to-bottom — the first matching condition wins, all subsequent branches are skipped. Put the most specific threshold first (>=90 before >=80 before >=70). Truthy/falsy: False, 0, "", [], {}, None are falsy; everything else is truthy. Chained comparisons: 10 < x < 20 means 10 < x and x < 20.
Control flow pitfalls
- Deep
if/elifnesting — extract to functions or usematch/case - Redundant
elseafterreturn—if cond: return x; return yis cleaner - Mixing tabs and spaces — causes
IndentationError
Correct pattern
Extract deeply nested branches into named functions. Use early return to flatten logic: if not cond: return; do_work(). Configure your editor to use 4 spaces consistently — never mix tabs and spaces.
Runnable example for if / elif / else — indentation-based branching.
score = 85
if score >= 90:
grade = "A"
elif score >= 80:
grade = "B"
elif score >= 70:
grade = "C"
elif score >= 60:
grade = "D"
else:
grade = "F"
print(f"Score {score} → Grade {grade}")Score 85 → Grade BSimple if — single condition without else
A standalone if with no elif or else — the body runs only when the condition is truthy. Python allows the body on the same line for single statements.
Runnable example for Simple if — single condition without else.
x = 10
if x > 0: print(f"{x} is positive")10 is positiveTernary operator — inline conditional expression
Inline conditional: value_if_true if condition else value_if_false. Reads like natural English. Can nest, but readability drops fast — avoid nesting beyond 2 levels.
Runnable example for Ternary operator — inline conditional expression.
age = 20
status = "adult" if age >= 18 else "minor"
print(f"age={age} → {status}")age=20 → adultNested ternary — chained inline conditions
Ternary expressions can chain: a if c1 else b if c2 else c. Readability drops fast — avoid nesting beyond 2 levels. For 3+ tiers, use if/elif/else or match/case.
Runnable example for Nested ternary — chained inline conditions.
val = 15
label = "high" if val > 20 else "mid" if val > 10 else "low"
print(f"val={val} → {label}")val=15 → midTruthy/falsy and chained comparisons
Truthy/falsy
if items:—Truefor non-empty collections- Falsy values:
0,0.0,"",None,[],{},set() - Chained comparisons:
0 < x < 100evaluatesxonly once and/orreturn operands:name = user or "Anonymous"
Don’t use if x == True
Don’t use if x == True — just if x. But be careful with truthy checks when 0 or "" is legitimate data — be explicit in those cases.
Correct pattern
Use if x: for truthy checks. When 0, "", or False are valid data values, be explicit: if x is not None: or if count != 0:. Reserve if x == True / if x is True only when you need to distinguish True from other truthy values.
Runnable example for Truthy/falsy and chained comparisons.
items = [1, 2, 3]
if items:
print(f"List has {len(items)} items")
name = ""
if not name:
print("Name is empty")
value = None
if value is None:
print("Value is None")
x = 15
if 10 < x < 20:
print(f"{x} is between 10 and 20")List has 3 items
Name is empty
Value is None
15 is between 10 and 20Pattern matching with match / case
Python 3.10 introduced structural pattern matching with match/case. Unlike C#‘s switch, Python’s match destructures values, binds variables, and tests types in one step — the pattern shape IS the condition.
match/case — pattern matching
Pattern matching (Python 3.10+)
match/case— tests against patterns, not just equality|for OR,_for wildcard,iffor guards, variable binding- First match wins
Bare variable names in case
Bare variable names in case capture (don’t compare) — use literals or guards. Don’t forget the _ default — unmatched values silently pass through.
Correct pattern
Use string literals for equality: case "start":. For variable comparison, use a guard: case cmd if cmd == expected:. Always add a case _: wildcard as the final branch to handle unmatched values explicitly.
Runnable example for match/case — pattern matching.
command = "quit"
match command:
case "start":
print("Starting...")
case "stop" | "quit" | "exit":
print("Stopping...")
case str(cmd) if cmd.startswith("go"):
cmd
case _:
commandStopping...match with destructuring
case (x, 0) binds x from a 2-tuple where second is 0. case {"key": val} matches dict structure. Combines validation and extraction in one step — the shape IS the condition.
Runnable example for match with destructuring.
point = (3, 0)
match point:
case (0, 0):
print("Origin")
case (x, 0):
print(f"On x-axis at {x}")
case (0, y):
print(f"On y-axis at {y}")
case (x, y):
print(f"Point at ({x}, {y})")On x-axis at 3match with type checking
Type patterns
case int(n)— matches integers and binds ton- Combine with guards:
case int(n) if n > 0 - Replaces
isinstance()chains with clean pattern syntax
Runnable example for match with type checking.
def describe(value):
match value:
case int(n) if n > 0:
return f"positive int: {n}"
case int(n):
return f"non-positive int: {n}"
case str(s):
return f"string: '{s}'"
case [first, *rest]:
return f"list starting with {first}, {len(rest)} more"
case _:
return f"other: {type(value).__name__}"
for v in [42, -5, "hello", [1, 2, 3], 3.14]:
print(f" {str(v):12} → {describe(v)}") 42 → positive int: 42
-5 → non-positive int: -5
hello → string: 'hello'
[1, 2, 3] → list starting with 1, 2 more
3.14 → other: floatLoops
Python provides two loop constructs: for (iterates any iterable) and while (condition-driven). There is no C-style for(i=0; i<n; i++) — use range() instead. Python has no do-while — use while True: ... if cond: break. The for/else construct runs the else block only when no break occurred.
Decision flow for choosing between for, while, range(), and value-producing iteration.
flowchart TD A["What drives the iteration?"] --> B["A collection or iterable"] A --> C["A numeric range"] A --> D["A condition"] A --> E["A transformation"] B --> F["for item in iterable"] C --> G["for i in range(...)"] D --> H["while condition"] E --> I["Comprehension or<br>map/filter"] F --> J["Preferred: direct,<br>no index needed"] G --> K["Use range() for<br>counted loops"] H --> L["Polling, retry,<br>input validation"] I --> M["Lazy, composable,<br>value-returning"]
for loop and iterables
The for loop iterates any object implementing the iterator protocol (__iter__/__next__): lists, tuples, strings, dicts, ranges, generators, and files. Use enumerate() for indices and zip() for parallel iteration.
for loops — iteration over iterables
Loop types
for— iterates over any iterable (list, range, dict, generator)while— repeats until the condition is falsefor i in range(n)— replaces C-stylefor(i=0; i<n; i++)for/else— theelseblock runs only if nobreakoccurred- No
do-while— usewhile True: ... if cond: break
Loop anti-patterns
for i in range(len(items))— usefor item in itemsorenumerate()while Truewithoutbreak— always have an exit condition- Modifying a list during iteration — use a copy or comprehension
Correct pattern
Iterate directly: for item in items: or with index: for i, item in enumerate(items):. For while True, always include a clear exit: if condition: break. To filter during iteration, build a new list: items = [x for x in items if keep(x)].
Runnable example for for loops — iteration over iterables.
for fruit in ["apple", "banana", "cherry"]:
print(f" {fruit}") apple
banana
cherryGenerate sequences with range()
range() forms
range(stop),range(start, stop),range(start, stop, step)- Stop is exclusive; negative step for countdown
- Lazy — constant memory regardless of size;
500 in range(1000)is O(1)
Runnable example for Generate sequences with range().
for i in range(5):
print(f" {i}", end=" ")
print()
for i in range(2, 8):
print(f" {i}", end=" ")
print() 0 1 2 3 4
2 3 4 5 6 7range() with step — custom stride and countdown
The third argument sets the step. Positive step for skipping forward, negative step for counting down. The stop value is always exclusive.
Runnable example for range() with step — custom stride and countdown.
for i in range(0, 20, 3):
print(f" {i}", end=" ")
print()
for i in range(10, 0, -2):
print(f" {i}", end=" ")
print() 0 3 6 9 12 15 18
10 8 6 4 2Iterating strings and dicts — .items(), .values(), .keys()
Strings yield characters one at a time. Dicts yield keys by default; .items() for (key, value), .values() for values only. Don’t use for key in dict: dict[key] — use for k, v in dict.items().
Runnable example for Iterating strings and dicts — .items(), .values(), .keys().
for ch in "Hello":
print(f" '{ch}'", end=" ")
print()
d = {"name": "Alice", "age": 30, "city": "NYC"}
for key in d:
print(f" {key} = {d[key]}")
for key, value in d.items():
print(f" {key}: {value}") 'H' 'e' 'l' 'l' 'o'
name = Alice
age = 30
city = NYC
name: Alice
age: 30
city: NYCenumerate and zip
Enumerate and zip
-
enumerate(iterable, start=0)— yields(index, element) -
zip(a, b)— yields(a_i, b_i), stopping at the shortest -
Both are lazy; use
enumerateinstead ofrange(len(items)) -
Warning:
zipwith unequal lengths silently truncates — usezip_longestif needed.
Runnable example for enumerate and zip.
for i, fruit in enumerate(["apple", "banana", "cherry"]):
print(f" [{i}] {fruit}")
for i, fruit in enumerate(["apple", "banana"], start=1):
print(f" [{i}] {fruit}") [0] apple
[1] banana
[2] cherry
[1] apple
[2] bananaParallel iteration with zip
zip(a, b) yields (a_i, b_i) tuples, stopping at the shortest iterable. Use for lock-step iteration of parallel sequences — names with ages, keys with values, expected with actual.
Runnable example for Parallel iteration with zip.
names = ["Alice", "Bob", "Charlie"]
ages = [30, 25, 35]
for name, age in zip(names, ages):
print(f" {name} is {age}") Alice is 30
Bob is 25
Charlie is 35while loops and for / else
while repeats until the condition is false. Python’s unique for/else construct runs the else block only when no break occurred — useful for search patterns. Python has no do-while; use while True: ... if cond: break instead.
while — condition-first loop
while repeats until the condition is false. The body may never execute if the condition is false from the start. Use for polling, retry, input validation, and any loop where the iteration count is not known in advance.
Runnable example for while — condition-first loop.
count = 0
while count < 5:
print(f" count = {count}")
count += 1 count = 0
count = 1
count = 2
count = 3
count = 4for/else — search found/not found idiom
The else block runs only when no break occurred — it signals “search completed without finding a match.” Use exclusively for search patterns.
The else in for/else runs
The else in for/else runs when there’s no break — the name is counterintuitive. Don’t use it for non-search patterns.
Runnable example for for/else — search found/not found idiom.
for n in [2, 4, 6, 8]:
if n % 3 == 0:
print(f" Found multiple of 3: {n}")
break
else:
print(" No multiple of 3 found") Found multiple of 3: 6do-while workaround — while True with break
Python has no do-while. Use while True: body; if cond: break to guarantee at least one execution before checking the exit condition.
Runnable example for do-while workaround — while True with break.
while True:
val = 42
print(f" Got value: {val}")
if val > 0:
break Got value: 42Nested loops — Cartesian iteration
Nested for loops produce the Cartesian product of two ranges. For deeper nesting, prefer itertools.product() to keep the code flat and readable.
Runnable example for Nested loops — Cartesian iteration.
for i in range(3):
for j in range(3):
print(f" ({i},{j})", end="")
print() (0,0) (0,1) (0,2)
(1,0) (1,1) (1,2)
(2,0) (2,1) (2,2)Loop Control
Python provides break to exit a loop, continue to skip to the next iteration, and pass as a no-op placeholder. Python has no labeled break or goto — for multi-level exit, use a flag variable or extract to a function with return. The walrus operator (:=) enables assignment within loop conditions.
Control keywords
Keywords that alter loop execution and flow: break exits immediately, continue skips to the next iteration, pass is a no-op placeholder, and := enables inline assignment in conditions.
break — exit the innermost loop
Loop control
break— exits the innermost loop immediately (does NOT exit outer loops)continue— skips to the next iterationpass— no-op placeholder for empty blocks- Python has no labeled break — none of these affect outer loops
- Walrus operator (
:=) — assigns a value AND returns it in one expression
Runnable example for break — exit the innermost loop.
for i in range(10):
if i == 5:
print(f" Breaking at {i}")
break
print(f" {i}", end=" ") 0 1 2 3 4 Breaking at 5continue — skip to the next iteration
continue jumps to the next iteration, skipping the remaining body. Use for filtering within a loop when a comprehension is not practical — avoids nested if/else blocks.
Runnable example for continue — skip to the next iteration.
for i in range(10):
if i % 2 == 0:
continue
print(f" {i}", end=" ") 1 3 5 7 9pass — no-op placeholder
pass is a no-op statement for syntactically required but intentionally empty blocks: stubs, placeholder classes, and except blocks during development.
Don’t use pass in production
Don’t use pass in production except blocks — at minimum log the error.
Correct pattern
In except blocks, always handle or log: except ValueError as e: logger.warning("Invalid input: %s", e). Use pass only as a temporary placeholder during development or for intentionally empty class/function stubs.
Runnable example for pass — no-op placeholder.
for i in range(5):
if i == 3:
pass
else:
print(f" {i}", end=" ")
class Placeholder:
pass 0 1 2 4Nested break behavior — only exits the innermost loop
In nested loops, break affects only the innermost loop — outer loops continue. Python has no labeled break. For multi-level exit, use a flag + break, or extract to a function and return.
Runnable example for Nested break behavior — only exits the innermost loop.
for i in range(3):
for j in range(3):
if j == 2:
break
print(f" ({i},{j})", end="")
print() (0,0) (0,1)
(1,0) (1,1)
(2,0) (2,1)Breaking outer loops with a flag
Set a flag variable in the inner loop, then check it in the outer loop. Verbose but explicit — works when extraction to a function is not practical.
Runnable example for Breaking outer loops with a flag.
found = False
for i in range(3):
for j in range(3):
if i == 1 and j == 1:
found = True
break
if found:
break
print(f" Broke at ({i},{j})") Broke at (1,1)Breaking outer loops with return
Wrap nested loops in a function and use return to exit all loops at once. Cleaner and more Pythonic than flag variables.
Runnable example for Breaking outer loops with return.
def find_pair():
for i in range(3):
for j in range(3):
if i == 1 and j == 1:
return (i, j)
return None
print(find_pair())(1, 1)Walrus operator — :=
(var := expr) assigns and returns the value in one expression. Eliminates the “read-before-loop” duplication. Also works in comprehensions: [y for x in data if (y := f(x)) > 0]. Don’t overuse — simple assignments are clearer with =.
Runnable example for Walrus operator — :=.
reader = io.StringIO("line1\nline2\nline3\n")
while (line := reader.readline()):
print(f" '{line.strip()}'") 'line1'
'line2'
'line3'Walrus in if conditions and comprehensions
:= in an if condition assigns and tests in one expression — eliminates a separate assignment line. In comprehensions, it captures an intermediate computation for reuse in both the filter and the output expression.
Runnable example for Walrus in if conditions and comprehensions.
data = "Hello World"
if (n := len(data)) > 5:
print(f" String has {n} chars (> 5)")
results = [y for x in range(10) if (y := x ** 2) > 20]
print(results) String has 11 chars (> 5)
[25, 36, 49, 64, 81]Iterators & Generators
Generator functions use yield to produce values lazily — Python suspends execution at each yield and resumes on the next next() call. This enables memory-efficient processing of large or infinite sequences, composable pipelines, and custom traversal logic. yield from delegates to sub-generators in a single expression — Python’s equivalent to C#‘s foreach (var x in sub) yield return x.
Generator functions and expressions
yield turns a function into a generator. Generator expressions (x for x in ...) are the lazy counterpart to list comprehensions. Both are single-use — exhausted after one pass.
Generator functions — yield for lazy sequences
A function with yield becomes a generator. Each next() call resumes execution until the next yield — state is preserved between calls. Values are computed lazily (on demand, not upfront). Generators are single-use — exhausted after one pass.
Key concepts: yield from delegates to sub-generators, generator expressions (x for x in ...) are lazy comprehensions, StopIteration signals exhaustion, and the iterator protocol requires __iter__() + __next__().
Generator pitfalls
- Returning a list when
yieldwould be lazier - Calling
list()on a generator just to iterate — defeats lazy evaluation - Generators are single-use — exhausted after one pass
Correct pattern
Use yield to return values lazily: def gen(): yield item. Iterate directly with for item in gen(): — no need to call list() first. If you need multiple passes, call the generator function again to create a fresh iterator.
Runnable example for Generator functions — yield for lazy sequences.
def countdown(n):
print(f" Starting countdown from {n}")
while n > 0:
yield n
n -= 1
print(" Done!")
for val in countdown(5):
print(f" {val}", end=" ") Starting countdown from 5
5 4 3 2 1 Done!Manual iteration with next()
next(gen) returns the next yielded value. Raises StopIteration when exhausted — use next(gen, default) to return a default instead. Use for peeking at the first element or partial consumption.
Runnable example for Manual iteration with next().
gen = countdown(3)
print(next(gen))
print(next(gen))
print(next(gen)) Starting countdown from 3
3
2
1List vs generator expression
List vs generator expression
[expr for x in iter]— creates a list in memory (eager)(expr for x in iter)— creates a generator (lazy, on-demand, constant memory)- As a function arg, parentheses can be omitted:
sum(x**2 for x in range(n)) - Use generators for large/streaming data; lists when you need indexing,
len(), or multiple passes
Runnable example for List vs generator expression.
squares_list = [x**2 for x in range(10)]
print(squares_list)[0, 1, 4, 9, 16, 25, 36, 49, 64, 81]Generator expression — lazy on-demand evaluation
(expr for x in iter) creates a generator that computes values on demand. As a function argument, outer parentheses can be omitted: sum(x**2 for x in range(n)). Generators are single-use — exhausted after one pass.
Runnable example for Generator expression — lazy on-demand evaluation.
squares_gen = (x**2 for x in range(10))
print(type(squares_gen).__name__)
print(list(squares_gen))generator
[0, 1, 4, 9, 16, 25, 36, 49, 64, 81]Memory comparison — list vs generator
The list stores all 100,000 values in memory (~800 KB), while the generator object uses a constant ~192 bytes regardless of how many values it will produce. Use generators for large or streaming data; lists when you need indexing, len(), or multiple passes.
Runnable example for Memory comparison — list vs generator.
big_list = [x for x in range(100000)]
big_gen = (x for x in range(100000))
print(f"List size: {sys.getsizeof(big_list):>8} bytes")
print(f"Generator size: {sys.getsizeof(big_gen):>8} bytes")List size: 800984 bytes
Generator size: 192 bytesyield from
yield from iterable replaces for item in iterable: yield item in one line. Enables recursive generators (flatten) and delegation to sub-generators. Watch out: yield from on strings yields each character separately, and deep recursion may hit the limit.
Runnable example for yield from.
def flatten(nested):
for item in nested:
if isinstance(item, list):
yield from flatten(item)
else:
yield item
nested = [1, [2, 3], [4, [5, 6]], 7]
print(list(flatten(nested)))[1, 2, 3, 4, 5, 6, 7]Infinite generators and itertools
Infinite generators use while True with yield to produce unbounded sequences. The itertools module provides composable, memory-efficient iterator building blocks. All are lazy — values are computed on demand.
Infinite generator with yield
while True with yield produces infinite values. Callers control consumption with islice, break, or zip. Zero storage — values computed on demand.
Never call list() or len()
Never call list() or len() on an infinite generator — hangs or OOM. Always limit with islice or break.
Correct pattern
Use itertools.islice to safely take a finite number of values: list(islice(naturals(), 10)). In loops, use break to exit when the desired condition is met: for n in naturals(): if n > 100: break.
Runnable example for Infinite generator with yield.
def naturals(start=0):
n = start
while True:
yield n
n += 1
print(list(islice(naturals(), 5)))
print(list(islice(naturals(10), 5)))[0, 1, 2, 3, 4]
[10, 11, 12, 13, 14]Built-in lazy iterators — range, enumerate, zip, map, filter, reversed
range, enumerate, zip, map, filter, and reversed are all lazy built-in iterators — wrap in list() to materialize. They consume constant memory regardless of input size.
Runnable example for Built-in lazy iterators — range, enumerate, zip, map, filter, reversed.
print(list(range(5)))
print(list(enumerate('abc')))
print(list(zip([1,2], ['a','b'])))
print(list(map(str.upper, ['a','b'])))
print(list(filter(lambda x: x > 2, [1,2,3,4])))
print(list(reversed([1,2,3])))[0, 1, 2, 3, 4]
[(0, 'a'), (1, 'b'), (2, 'c')]
[(1, 'a'), (2, 'b')]
['A', 'B']
[3, 4]
[3, 2, 1]itertools — chain, cycle, repeat, accumulate, product
Key itertools functions (all lazy generators)
-
chain— joins iterables end-to-end -
cycle— repeats infinitely -
repeat— yields same value n times -
accumulate— computes running totals -
product— Cartesian product -
Warning: Never
list(cycle(...))— infinite memory.
Runnable example for itertools — chain, cycle, repeat, accumulate, product.
print(list(chain([1,2], [3,4])))
print(list(repeat('x', 3)))
print(list(accumulate([1,2,3,4])))
print(list(product('ab', '12')))[1, 2, 3, 4]
['x', 'x', 'x']
[1, 3, 6, 10]
[('a', '1'), ('a', '2'), ('b', '1'), ('b', '2')]Iterator protocol — iter and next
Define __iter__(self) returning self and __next__(self) raising StopIteration when done. Makes any class usable in for loops, list(), and all iteration contexts. Use for complex stateful iteration — for simple sequences, generator functions are much less code.
Runnable example for Iterator protocol — __iter__ and __next__.
class Squares:
def __init__(self, n):
self.n = n
self.i = 0
def __iter__(self):
return self
def __next__(self):
if self.i >= self.n:
raise StopIteration
val = self.i ** 2
self.i += 1
return val
print(list(Squares(5)))[0, 1, 4, 9, 16]Flattening nested structures
Flattening converts nested collections into a single flat sequence. Python offers multiple approaches depending on depth and dependencies: chain.from_iterable (1 level), more_itertools.collapse (any depth), stack-based iterative (no dependencies), and pd.json_normalize (nested dicts).
Flatten nested iterables — four approaches
Use chain.from_iterable() for one-level nested iterables. Use collapse() when you need arbitrary-depth flattening and can accept a third-party dependency. Use an explicit stack when you want no extra dependency and no recursion depth limit. Use pd.json_normalize() when the input is nested record data rather than nested lists.
itertools chain.from_iterable and more_itertools collapse
chain.from_iterable flattens exactly one level — inner lists stay nested. Stdlib, lazy, no external dependency. For arbitrary depth, use more_itertools.collapse.
Runnable example for itertools chain.from_iterable and more_itertools collapse.
one_level = list(chain.from_iterable([[1, 2], [3, 4], [5, 6]]))
print(one_level)[1, 2, 3, 4, 5, 6]more_itertools collapse — arbitrary depth
collapse from more_itertools flattens any depth of nesting in one call. External dependency, but the simplest solution for deeply nested structures.
Runnable example for more_itertools collapse — arbitrary depth.
from more_itertools import collapse
nested = [1, [2, 3], [4, [5, 6]], 7]
print(list(collapse(nested)))[1, 2, 3, 4, 5, 6, 7]Iterative Flatten with Stack
Stack-based iterative flatten — no recursion, no depth limits, handles arbitrarily deep nesting safely. Watch out: strings are iterable and cause infinite loops if not checked.
Runnable example for Iterative Flatten with Stack.
def flatten_iter(nested):
"""Flatten using an explicit stack — no recursion needed."""
stack = list(reversed(nested))
result = []
while stack:
item = stack.pop()
if isinstance(item, list):
stack.extend(reversed(item))
else:
result.append(item)
return result
nested = [1, [2, 3], [4, [5, 6]], 7]
print(flatten_iter(nested))[1, 2, 3, 4, 5, 6, 7]pandas json_normalize
pd.json_normalize takes a list of nested dicts, flattens nested keys into dot-separated column names, and handles missing keys with NaN. One-line flatten for API responses and JSON files.
Runnable example for pandas json_normalize.
import pandas as pd
nested_records = [
{"name": "Alice", "address": {"city": "NYC", "zip": "10001"}},
{"name": "Bob", "address": {"city": "LA", "zip": "90001"}},
]
df = pd.json_normalize(nested_records)
print(df) name address.city address.zip
0 Alice NYC 10001
1 Bob LA 90001Flatten approach summary
| Scenario | Approach |
|---|---|
| 1 level deep | list(chain.from_iterable(nested)) |
| Any depth | list(collapse(nested)) (more-itertools) |
| No dependencies | Iterative with stack (no recursion needed) |
| Nested JSON | pd.json_normalize(records) |
Comprehensions & Functional Tools
Comprehensions are Python’s declarative syntax for building lists, dicts, and sets in a single expression. They replace imperative for/append patterns with concise, readable, and faster alternatives. Functional tools (map, filter, reduce, sorted) provide composable transformations — prefer comprehensions with lambdas, but use map/filter when you already have a named function.
Comprehensions
List, dict, and set comprehensions build new collections from iterables with optional filtering. They are optimized at bytecode level and faster than equivalent for loops.
List comprehension — concise collection building
[expr for item in iterable if condition] — builds a new list by applying an expression to each element, optionally filtering with if. More readable than map/filter/lambda and faster than equivalent for loops (optimized at bytecode level).
Don’t use comprehensions for side Don’t use comprehensions for side effects (printing, writing). Don’t nest beyond 2 levels — use explicit loops instead.
Correct pattern
Use comprehensions only to build collections: squares = [x**2 for x in range(10)]. For side effects (printing, writing, mutating), use an explicit for loop. Keep nesting to 2 levels maximum; beyond that, extract the inner logic into a named function.
Runnable example for List comprehension — concise collection building.
squares = [x**2 for x in range(10)]
print(squares)[0, 1, 4, 9, 16, 25, 36, 49, 64, 81]List comprehension with filter — if clause
Adding if condition filters elements before the expression is applied. Only elements satisfying the predicate appear in the output list.
Runnable example for List comprehension with filter — if clause.
evens = [x for x in range(20) if x % 2 == 0]
print(evens)[0, 2, 4, 6, 8, 10, 12, 14, 16, 18]List comprehension with filter and transform
Combine if filtering with an expression transform in a single comprehension — the Pythonic equivalent of a .Where().Select() chain.
Runnable example for List comprehension with filter and transform.
words = ["hello", "world", "python", "is", "great"]
long_upper = [w.upper() for w in words if len(w) > 3]
print(long_upper)['HELLO', 'WORLD', 'PYTHON', 'GREAT']Nested comprehensions
[expr for outer in iter1 for inner in iter2] — outer loop first, then inner (same order as nested for loops). One-line flatten: [n for row in matrix for n in row]. Don’t nest beyond 2 levels.
Runnable example for Nested comprehensions.
matrix = [[1, 2, 3], [4, 5, 6], [7, 8, 9]]
flat = [n for row in matrix for n in row]
print(flat)
grid = [[(i, j) for j in range(3)] for i in range(3)]
print(grid)[1, 2, 3, 4, 5, 6, 7, 8, 9]
[[(0, 0), (0, 1), (0, 2)], [(1, 0), (1, 1), (1, 2)], [(2, 0), (2, 1), (2, 2)]]Dict and set comprehensions
Dict and set comprehensions
-
{k: v for item in iterable}— builds a dict -
{expr for item}— builds a set (auto-deduplicates) -
Both support
iffiltering -
Invert a dict:
{v: k for k, v in d.items()} -
Warning: Dict with duplicate keys — last value wins silently.
Runnable example for Dict and set comprehensions.
squares_dict = {x: x**2 for x in range(6)}
print(squares_dict)
original = {"a": 1, "b": 2, "c": 3}
swapped = {v: k for k, v in original.items()}
print(swapped)
scores = {"Alice": 85, "Bob": 92, "Charlie": 78, "Diana": 95}
passed = {name: score for name, score in scores.items() if score >= 80}
print(passed){0: 0, 1: 1, 2: 4, 3: 9, 4: 16, 5: 25}
{1: 'a', 2: 'b', 3: 'c'}
{'Alice': 85, 'Bob': 92, 'Diana': 95}Set comprehension — auto-deduplicated collection
{expr for item in iterable} builds a set — automatically deduplicates. Use for extracting unique values from a sequence.
Runnable example for Set comprehension — auto-deduplicated collection.
words = ["hello", "world", "python", "is", "great"]
unique_lengths = {len(w) for w in words}
print(unique_lengths){2, 5, 6}Functional programming
map, filter, and reduce provide functional-style transformations. Built-in aggregations (sum, max, min, any, all) are preferred over reduce for common operations — they are implemented in C and short-circuit where applicable.
map and filter
Map and filter
map(func, iterable)— appliesfuncto every elementfilter(pred, iterable)— keeps elements wherepredis True- Both are lazy; best with named functions (
map(str.upper, words)) - With lambdas, comprehensions are almost always clearer
Runnable example for map and filter.
nums = [1, 2, 3, 4, 5]
doubled = list(map(lambda x: x * 2, nums))
print(doubled)
doubled2 = [x * 2 for x in nums]
print(doubled2)[2, 4, 6, 8, 10]
[2, 4, 6, 8, 10]filter — keep elements matching a predicate
filter(pred, iterable) keeps elements where pred returns True. Lazy — wrap in list() to materialize. With lambdas, a list comprehension with if is usually clearer.
Runnable example for filter — keep elements matching a predicate.
nums = [1, 2, 3, 4, 5]
evens = list(filter(lambda x: x % 2 == 0, nums))
print(evens)[2, 4]reduce — general-purpose fold
reduce(func, iterable, initial) applies func cumulatively, folding the sequence into a single value. The third argument is the initial accumulator. Use for custom reductions that built-in functions don’t cover.
Runnable example for reduce — general-purpose fold.
nums = [1, 2, 3, 4, 5]
total = reduce(lambda acc, x: acc + x, nums, 0)
print(total)
product = reduce(lambda acc, x: acc * x, nums, 1)
print(product)15
120Built-in aggregations — sum, max, min, any, all
Prefer built-ins over reduce for common operations — they are implemented in C and short-circuit where applicable. any() stops on the first True; all() stops on the first False.
Runnable example for Built-in aggregations — sum, max, min, any, all.
nums = [1, 2, 3, 4, 5]
print(sum(nums))
print(max(nums))
print(min(nums))
print(all(x > 0 for x in nums))
print(any(x > 3 for x in nums))15
5
1
True
Truesorted() with key function — custom sort order, multi-key, reverse
Sorting
sorted(iterable, key=func)— returns a new sorted listlist.sort()— sorts in placekeyextracts the comparison value:key=len,key=str.lower,key=lambda x: x[1]reverse=True— descending order- Python’s sort is stable — equal elements keep original order
- Multiple sort keys — return a tuple:
key=lambda x: (x[0], -x[1])
Runnable example for sorted() with key function — custom sort order, multi-key, reverse.
names = ["Charlie", "Alice", "Bob", "Diana"]
print(sorted(names))
print(sorted(names, key=len))
print(sorted(names, reverse=True))
print(sorted(names, key=lambda n: n[-1]))['Alice', 'Bob', 'Charlie', 'Diana']
['Bob', 'Alice', 'Diana', 'Charlie']
['Diana', 'Charlie', 'Bob', 'Alice']
['Diana', 'Bob', 'Charlie', 'Alice']Selecting the right iteration construct
Use a comprehension when one expression transforms or filters data into a new collection. Keep it to one or two for clauses, and stop once the expression needs branching or side effects.
Use a for loop when the body mutates state, performs I/O, handles errors, or needs several statements. It is also the clearest choice when early break or continue matters.
Use map() or filter() when you already have a named function and want lazy composition. With inline lambdas, the equivalent comprehension is usually easier to read.
Use a generator when the result can stay lazy, the input is large, or downstream code consumes values once. Materialize with list() only when you need repeated passes, indexing, or len().
Common failure modes
Syntax and indentation errors.
IndentationError usually means tabs and spaces were mixed or the block depth changed unexpectedly. SyntaxError: expected ':' means a block opener such as if, for, while, def, or match is missing its trailing colon.
Iterator and generator exhaustion.
A generator raises StopIteration after its values are consumed. Re-call the generator function for a fresh iterator, or convert the generator to a list once when the data must be reused.
Loop mutation and control-flow surprises.
Modifying a list during iteration can skip elements, and mutating a dict or set during iteration can raise RuntimeError. Iterate over a copy or build a new collection instead. Remember that for...else runs its else block on normal completion, not when a loop condition becomes false, and ensure every while loop has a state change or explicit break.
Readability, side effects, and exception scope.
zip() stops at the shortest iterable; use itertools.zip_longest() when uneven input lengths are expected. A comprehension that calls a side-effect function can produce None values or hide control flow, so use an explicit loop when readability or sequencing matters. Avoid bare except in loop-oriented code paths; catch specific exceptions so control flow does not mask defects. Once a comprehension needs more than two for clauses, move the inner logic into a function or a loop.
Version and dependency boundaries.
match/case requires Python 3.10 or newer. The arbitrary-depth flattening example requires more-itertools, and the JSON flattening example requires pandas; if those packages are unavailable, use the stack-based flattener or standard-library tools instead.