15 min read

> "If you have to do something more than twice, automate it."

Chapter 5: Loops and Iteration — Automating Repetitive Tasks

"If you have to do something more than twice, automate it." — A principle older than computers


Opening Scenario: The Monday Morning Report

Every Monday morning, Sandra Chen receives a regional sales summary by 9:30 a.m. It covers four regions, the previous week's figures, cumulative totals, and a comparison against targets. It is two pages. It has been produced manually, every Monday, for six years.

Priya Okonkwo — Acme Corp's junior analyst — owns the process. She opens four Excel tabs, copies the figures into a summary template, calculates the formulas, checks for errors, formats the table, and emails it. The process takes between 35 and 55 minutes depending on whether the data is clean.

Priya told Marcus Webb last Tuesday that she spends more time producing the report than thinking about what it says.

Marcus suggested she write a loop.


This chapter is about loops: Python's mechanism for repeating a block of code over a sequence of data or until a condition is met. Loops are not a feature you learn and move on from. They are the foundation of virtually every useful program: processing records, generating reports, analyzing datasets, sending notifications, updating databases. Nearly everything a computer does that earns its keep involves some form of repetition.

You will notice something shift in this chapter. The programs you wrote in Chapters 3 and 4 were static — they operated on specific values and produced a specific output. Loops introduce iteration: the ability for a single piece of code to process any number of items. Write the logic once; it scales to ten records or ten million without modification.

That is the automation mindset. Let's build it.


5.1 The Automation Mindset: Spotting Loop Opportunities

Before you write a single line of loop code, it is worth developing the instinct to recognize when a loop is the right tool. Business workflows contain these patterns constantly, but they are easy to miss when you are used to doing them by hand.

Ask yourself these questions about any task:

  1. Is there a list of things? Customers, invoices, orders, regions, employees, SKUs, dates — anything that comes as a collection. If you have a list of 200 items and you need to do the same thing to each one, that is a loop.

  2. Does the same logic repeat? If you are doing the same calculation twelve times (once per month), or the same check four times (once per region), that is a loop.

  3. Is there a running total? Any time you are accumulating a sum, a count, or a maximum over multiple items, a loop handles it cleanly.

  4. Does something continue until a condition is met? "Keep processing invoices until the total reaches $50,000" or "keep prompting until valid input is received" — these are while loops.

At Acme Corp, Priya's Monday report touches all four patterns: a list of regions, the same calculation repeated per region, a running total, and a process that continues until all regions are processed. One loop (or a small set of them) replaces forty minutes of manual work.


5.2 for Loops: Iterating Over Sequences

The for loop is Python's workhorse for processing collections. Its structure is simple:

for item in collection:
    # code to run for each item

Python takes each element from collection, assigns it to item, runs the indented block, then moves to the next element. When the collection is exhausted, the loop ends and execution continues with the code below.

Iterating Over a List

regions = ["Northeast", "Southeast", "Midwest", "West Coast"]

for region in regions:
    print(f"Processing: {region}")

Output:

Processing: Northeast
Processing: Southeast
Processing: Midwest
Processing: West Coast

Four items in the list, four executions of the loop body. The variable name region is arbitrary — it is created by the for statement and holds one item per iteration. Use a meaningful name that reflects the type of item in the collection.

A More Realistic Example

regional_sales = [145_200, 98_400, 112_600, 187_300]
total_sales = 0

for sales_figure in regional_sales:
    total_sales += sales_figure

print(f"Total sales across all regions: ${total_sales:,.2f}")

Output:

Total sales across all regions: $543,500.00

This is the accumulator pattern: initialize a variable before the loop (here total_sales = 0), add to it inside the loop, and read the final value after the loop ends. You will use this pattern constantly.

Iterating Over a List of Dictionaries

Real business data rarely comes as a flat list of numbers. More commonly, it comes as a list of records — each record being a dictionary. for loops handle this naturally:

invoices = [
    {"id": "INV-001", "client": "Northgate Supplies", "amount": 12_500, "paid": True},
    {"id": "INV-002", "client": "Westbrook Mfg", "amount": 8_750, "paid": False},
    {"id": "INV-003", "client": "Clearview Logistics", "amount": 22_000, "paid": False},
]

total_outstanding = 0

for invoice in invoices:
    if not invoice["paid"]:
        total_outstanding += invoice["amount"]
        print(f"  Outstanding: {invoice['id']} — ${invoice['amount']:,.2f}")

print(f"\nTotal outstanding: ${total_outstanding:,.2f}")

Output:

  Outstanding: INV-002 — $8,750.00
  Outstanding: INV-003 — $22,000.00

Total outstanding: $30,750.00

The for loop assigns the entire dictionary to invoice each time. You access individual fields using invoice["field_name"] just as you would outside a loop.

Iterating Over Strings

Strings are sequences too — Python can iterate over individual characters:

product_code = "ACME-NE-2024-Q4"

for character in product_code:
    if character == "-":
        print("  [separator]")
    else:
        print(f"  {character}")

This is less common in business code than list iteration, but it comes up when parsing identifiers, codes, or formatted text.


5.3 The range() Function: Numeric Iteration

The range() function generates a sequence of integers, which is useful when you need to iterate a specific number of times or work with numeric indices.

range(stop)           # 0, 1, 2, ..., stop-1
range(start, stop)    # start, start+1, ..., stop-1
range(start, stop, step)  # start, start+step, start+2*step, ...

Basic Usage

# Iterate exactly 5 times
for quarter_num in range(1, 6):
    print(f"  Q{quarter_num}")

Output:

  Q1
  Q2
  Q3
  Q4
  Q5

Business Use: Monthly Report Loop

monthly_targets = [50_000, 52_000, 48_000, 55_000, 60_000, 58_000,
                   62_000, 64_000, 59_000, 67_000, 72_000, 75_000]

month_names = ["Jan", "Feb", "Mar", "Apr", "May", "Jun",
               "Jul", "Aug", "Sep", "Oct", "Nov", "Dec"]

annual_target = 0

for month_index in range(len(monthly_targets)):
    target = monthly_targets[month_index]
    month = month_names[month_index]
    annual_target += target
    print(f"  {month}: ${target:>8,.0f}")

print(f"\n  Annual target: ${annual_target:,.0f}")

This works, but it is not the Python idiom you will usually reach for. When you need both the index and the value, enumerate() (Section 5.8) is cleaner. range(len(...)) is fine but should not be your first instinct.

Counting Down and Using Step

# Count down from 10 to 1 (step=-1)
for countdown in range(10, 0, -1):
    print(countdown, end=" ")
print("Launch!")

# Every other week in a 52-week year
for week in range(0, 52, 2):
    print(f"Bi-weekly report: week {week + 1}")

A step of -1 counts down. A step of 2 skips every other value. These are not exotic features — week-over-week or bi-monthly patterns come up regularly in business reporting.


5.4 while Loops: Condition-Based Repetition

A while loop runs as long as a condition is true. Unlike a for loop, which iterates over a fixed collection, a while loop continues indefinitely until its condition becomes false.

while condition:
    # code runs repeatedly until condition is False

Basic Example

balance = 10_000
monthly_payment = 750
month = 0

while balance > 0:
    balance -= monthly_payment
    month += 1
    if balance < 0:
        balance = 0   # Don't go negative on the last payment
    print(f"  Month {month:>2}: Remaining balance ${balance:>8,.2f}")

print(f"\nLoan paid off in {month} months.")

Output (truncated):

  Month  1: Remaining balance $  9,250.00
  Month  2: Remaining balance $  8,500.00
  ...
  Month 13: Remaining balance $    250.00
  Month 14: Remaining balance $      0.00

Loan paid off in 14 months.

This is a natural while loop — the number of iterations is not known in advance. You continue making payments until the balance hits zero.

The "Process Until Done" Pattern

A common pattern processes items from a list using an index variable:

sales_entries = [1200, 3400, 900, 2800, 1500, 4200]
index = 0
total = 0

while index < len(sales_entries):
    entry = sales_entries[index]
    total += entry
    index += 1

print(f"Total: ${total:,}")

For simple sequences, a for loop is cleaner. But the index-based while loop is useful when you need to look at two entries at once (compare index and index + 1) or when you need to jump forward more than one step conditionally.

Warning: Infinite Loops

A while loop whose condition never becomes false runs forever. This is the most common beginner mistake with while loops, and it will hang your program:

# WARNING: This loop never ends. Don't run it.
count = 1
while count > 0:
    count += 1    # count keeps growing, always > 0
    print(count)

Always make sure something inside your while loop will eventually make the condition false. Usually this means modifying the variable the condition tests.


5.5 break, continue, and pass

Python provides three keywords that give you fine-grained control over loop execution.

break: Exit a Loop Early

break immediately terminates the loop and jumps to the code after it. Use it when you have found what you were looking for and there is no reason to continue iterating.

# Find the first invoice over $20,000
invoices = [8_500, 12_400, 6_200, 31_000, 9_800, 45_000]
large_invoice = None

for amount in invoices:
    if amount > 20_000:
        large_invoice = amount
        break   # Found it — stop searching

if large_invoice:
    print(f"First large invoice: ${large_invoice:,.2f}")

Output:

First large invoice: $31,000.00

Without break, the loop would continue through the remaining invoices even after the answer was found. On a 2,000-record dataset this matters. The break keyword is honest about intent: when you only need the first match, stop after the first match.

Business uses for break: - Find the first transaction that exceeds a fraud threshold - Stop processing once a budget cap is reached - Exit a data validation loop as soon as an error is found

continue: Skip to the Next Iteration

continue skips the rest of the current iteration's code and jumps directly to the next item. The loop does not stop — it just skips one step.

sales_entries = [
    {"rep": "Ana Lima", "amount": 8_400, "approved": True},
    {"rep": "Tom Park", "amount": 12_200, "approved": False},
    {"rep": "Jess Wong", "amount": 5_600, "approved": True},
    {"rep": "Dan Reyes", "amount": 9_100, "approved": False},
    {"rep": "Cara Bell", "amount": 15_300, "approved": True},
]

approved_total = 0

for entry in sales_entries:
    if not entry["approved"]:
        continue   # Skip unapproved entries — don't process them

    approved_total += entry["amount"]
    print(f"  Counted: {entry['rep']} — ${entry['amount']:,.0f}")

print(f"\nApproved total: ${approved_total:,.0f}")

Output:

  Counted: Ana Lima — $8,400
  Counted: Jess Wong — $5,600
  Counted: Cara Bell — $15,300

Approved total: $29,300

continue is often used as a guard clause: at the top of a loop body, check for conditions that should skip processing, then proceed with the normal logic. This reads more cleanly than wrapping the entire loop body in an if statement.

Business uses for continue: - Skip records with missing or null values - Ignore draft records when producing a final report - Filter out zero-quantity line items in an order report

pass: Do Nothing (a Placeholder)

pass is a no-op — it does exactly nothing. Its purpose is syntactic: Python requires at least one statement in any indented block, and pass satisfies that requirement when you do not have anything to put there yet.

regions = ["Northeast", "Southeast", "Midwest", "West Coast"]

for region in regions:
    if region == "Southeast":
        # TODO: Southeast data is pending — come back to this
        pass
    else:
        print(f"Processing {region}...")

In production code, pass is rare. You might encounter it in a loop stub that you intend to fill in, or in an exception handler where you explicitly want to ignore an error. Do not use it to silence errors you should be handling.


5.6 Nested Loops: Loops Inside Loops

A nested loop is a loop inside another loop. The inner loop runs completely for each iteration of the outer loop.

regions = ["Northeast", "Southeast", "Midwest"]
quarters = ["Q1", "Q2", "Q3", "Q4"]

for region in regions:
    for quarter in quarters:
        print(f"  {region} — {quarter}")

Output:

  Northeast — Q1
  Northeast — Q2
  Northeast — Q3
  Northeast — Q4
  Southeast — Q1
  ...

With 3 regions and 4 quarters, this produces 12 lines (3 × 4). The number of iterations multiplies: len(outer) × len(inner).

A Practical Example: Multi-Region, Multi-Period Report

regions = ["Northeast", "Southeast", "Midwest", "West Coast"]
quarterly_data = {
    "Northeast":  [145_200, 152_800, 138_500, 161_300],
    "Southeast":  [98_400,  103_700, 91_200,  107_800],
    "Midwest":    [112_600, 108_900, 119_300, 124_500],
    "West Coast": [187_300, 201_400, 195_600, 215_700],
}
quarters = ["Q1", "Q2", "Q3", "Q4"]

print(f"  {'Region':<14}", end="")
for quarter in quarters:
    print(f"{quarter:>12}", end="")
print(f"{'Annual':>12}")
print("-" * 66)

for region in regions:
    figures = quarterly_data[region]
    annual_total = sum(figures)
    print(f"  {region:<14}", end="")

    for figure in figures:     # Inner loop: print each quarter's figure
        print(f"${figure:>11,.0f}", end="")

    print(f"${annual_total:>11,.0f}")

The outer loop iterates over regions; the inner loop iterates over the quarters within each region. The result is a formatted grid.

When to Avoid Nested Loops

Nested loops multiply execution time. A loop over 1,000 records nested inside another loop over 1,000 records runs 1,000,000 iterations. For small datasets (hundreds of items) this is fine. For large datasets (tens of thousands), look for alternatives: dictionary lookups, list comprehensions, or pandas (covered in Chapter 10).

The rule of thumb: if your nested loop starts feeling slow or if either collection has more than a few thousand items, step back and think about whether a dictionary lookup or a library function can replace it.


5.7 List Comprehensions: Concise Transformation

A list comprehension is a compact syntax for building a new list by applying an expression to each item in an existing sequence — optionally filtering with a condition.

The Pattern

new_list = [expression for item in collection if condition]

The if condition part is optional. Without it, every item in collection is transformed by expression and added to new_list.

Building Intuition

Start with a for loop you already understand:

# For loop version
revenue_list = [145_200, 98_400, 112_600, 187_300]
revenue_in_thousands = []

for revenue in revenue_list:
    revenue_in_thousands.append(revenue / 1000)

print(revenue_in_thousands)
# [145.2, 98.4, 112.6, 187.3]

Now the same logic as a list comprehension:

# List comprehension version
revenue_in_thousands = [revenue / 1000 for revenue in revenue_list]
print(revenue_in_thousands)
# [145.2, 98.4, 112.6, 187.3]

One line instead of four. And once you can read the pattern, it is equally clear.

With a Filter

sales_figures = [8_400, 2_100, 34_000, 950, 12_500, 67_000, 4_200]

# Only figures above $10,000
large_sales = [amount for amount in sales_figures if amount > 10_000]
print(large_sales)
# [34000, 12500, 67000]

The if amount > 10_000 clause filters the sequence before applying the expression. Items that fail the condition are simply excluded.

Transforming Records

orders = [
    {"product": "Binders", "units": 150, "unit_price": 2.50},
    {"product": "Folders", "units": 240, "unit_price": 1.80},
    {"product": "Pens",    "units": 500, "unit_price": 0.45},
]

# Build a list of revenue figures for each order
order_revenues = [
    order["units"] * order["unit_price"]
    for order in orders
]
print(order_revenues)
# [375.0, 432.0, 225.0]

When to Use a List Comprehension vs. a for Loop

List comprehensions are excellent for transforming or filtering a sequence into a new list. Use them when the logic is simple enough to read naturally in one line. Use a regular for loop when:

  • The loop body has multiple steps
  • You need to update multiple variables
  • The logic includes nested conditions that would make a comprehension hard to read
  • You are accumulating something other than a list (use a loop for sums, maximums, etc.)

Comprehensions should make code clearer, not denser. If you have to squint to read it, break it into a for loop.


5.8 enumerate(): Index and Value Together

The enumerate() function wraps a sequence and yields both the index (position) and the value of each item. It eliminates the need to maintain a manual counter variable.

sales_reps = ["Ana Lima", "Tom Park", "Jess Wong", "Dan Reyes"]

for index, rep in enumerate(sales_reps):
    print(f"  {index}: {rep}")

Output:

  0: Ana Lima
  1: Tom Park
  2: Jess Wong
  3: Dan Reyes

By default, enumerate starts counting from 0. To start from 1 (which is more natural for numbered lists and reports), pass start=1:

for rank, rep in enumerate(sales_reps, start=1):
    print(f"  {rank}. {rep}")

Output:

  1. Ana Lima
  2. Tom Park
  3. Jess Wong
  4. Dan Reyes

Business Use: Ranked Report

regional_results = [
    ("West Coast", 1_008_900),
    ("Northeast", 756_500),
    ("Midwest", 596_500),
    ("Southeast", 513_400),
]

# Sort by sales descending first
regional_results.sort(key=lambda item: item[1], reverse=True)

print("REGIONAL RANKINGS")
print("-" * 35)
for rank, (region, total) in enumerate(regional_results, start=1):
    print(f"  #{rank}  {region:<16} ${total:>10,.0f}")

Output:

REGIONAL RANKINGS
-----------------------------------
  #1  West Coast       $1,008,900
  #2  Northeast          $756,500
  #3  Midwest            $596,500
  #4  Southeast          $513,400

enumerate is the idiomatic Python way to get a numbered sequence. Prefer it over range(len(collection)) whenever you need an index alongside a value.


5.9 zip(): Combining Multiple Sequences

The zip() function combines two or more sequences element by element, yielding tuples. When the sequences have different lengths, zip stops at the shortest one.

regions = ["Northeast", "Southeast", "Midwest", "West Coast"]
sales   = [756_500, 513_400, 596_500, 1_008_900]

for region, total in zip(regions, sales):
    print(f"  {region}: ${total:,.0f}")

Output:

  Northeast: $756,500
  Southeast: $513,400
  Midwest: $596,500
  West Coast: $1,008,900

zip makes it natural to work with parallel lists — lists where the items at the same index belong together. This is common when you have a list of labels, a separate list of values, and possibly a third list of targets.

Zipping Three Sequences

regions  = ["Northeast", "Southeast", "Midwest", "West Coast"]
actuals  = [756_500, 513_400, 596_500, 1_008_900]
targets  = [750_000, 500_000, 575_000, 1_000_000]

print(f"  {'Region':<14} {'Actual':>10} {'Target':>10} {'vs Target':>12}")
print("-" * 50)

for region, actual, target in zip(regions, actuals, targets):
    variance = actual - target
    print(
        f"  {region:<14} ${actual:>9,.0f} "
        f"${target:>9,.0f} ${variance:>+10,.0f}"
    )

Transposing a 2D List with zip(*...)

The zip(*iterable) idiom is worth learning. It transposes a 2D list — turning rows into columns and columns into rows.

# Each inner list is one week's regional figures
weekly_data = [
    [145_200, 98_400, 112_600, 187_300],   # Week 1
    [152_800, 103_700, 108_900, 201_400],  # Week 2
    [138_500, 91_200, 119_300, 195_600],   # Week 3
]

# Group by region (column) rather than by week (row)
for region_figures in zip(*weekly_data):
    print(f"  Regional total: ${sum(region_figures):,.0f}")

Output:

  Regional total: $436,500
  Regional total: $293,300
  Regional total: $340,800
  Regional total: $586,300

The * operator unpacks weekly_data into three separate arguments (the three inner lists). zip then groups the first element from each, the second from each, and so on — effectively grouping by column.


5.10 Common Business Loop Patterns

These are the patterns that appear in real business code over and over. Recognize them, and you can implement them quickly.

Summing: The Accumulator

total = 0
for value in values:
    total += value

Or with a list comprehension: total = sum(value for value in values). Note that Python's built-in sum() function handles simple cases without a loop.

Counting: How Many Match?

count = 0
for invoice in invoices:
    if invoice["overdue"]:
        count += 1

Or: count = sum(1 for inv in invoices if inv["overdue"]).

Filtering: Keep Only Matching Items

overdue_invoices = []
for invoice in invoices:
    if invoice["days_past_due"] > 30:
        overdue_invoices.append(invoice)

Or as a list comprehension: overdue_invoices = [inv for inv in invoices if inv["days_past_due"] > 30].

Transforming: Convert Each Item

amounts_in_euros = []
for amount_usd in amounts_usd:
    amounts_in_euros.append(amount_usd * 0.92)

Or: amounts_in_euros = [amount * 0.92 for amount in amounts_usd].

Finding: Locate the First Match

first_large_order = None
for order in orders:
    if order["total"] > 50_000:
        first_large_order = order
        break

Finding the Maximum or Minimum

largest_client = None
largest_revenue = 0

for client, revenue in client_revenues.items():
    if revenue > largest_revenue:
        largest_revenue = revenue
        largest_client = client

Python's built-in max() and min() can handle simple cases: max(client_revenues, key=client_revenues.get).

Building a Dictionary from a List

revenue_by_region = {}
for region, revenue in zip(regions, revenues):
    revenue_by_region[region] = revenue

Or as a dict comprehension: revenue_by_region = {r: v for r, v in zip(regions, revenues)}.

Grouping: Aggregate by Category

sales_by_rep = {}
for sale in all_sales:
    rep = sale["rep_name"]
    if rep not in sales_by_rep:
        sales_by_rep[rep] = 0
    sales_by_rep[rep] += sale["amount"]

This pattern — checking whether a key exists and initializing it if not, then accumulating — is one of the most common in business data processing.


5.11 Complete Example: Monthly Report Generator

Let's put everything together in a realistic, complete example. This is the kind of code Priya would actually write to automate her Monday morning report.

The script processes a list of regional data dictionaries and generates a formatted text report. It demonstrates for loops, enumerate, zip, list comprehensions, a nested loop, and the accumulator pattern — all working together.

"""
monthly_report_generator.py

Generates Acme Corp's monthly regional sales summary report.
Demonstrates for loops, enumerate, zip, list comprehensions,
nested loops, and accumulator patterns in a complete business context.
"""

# --- Data ---
regions = ["Northeast", "Southeast", "Midwest", "West Coast"]

monthly_sales = [
    # NE         SE         MW         WC
    [145_200, 98_400, 112_600, 187_300],   # January
    [152_800, 103_700, 108_900, 201_400],  # February
    [138_500, 91_200, 119_300, 195_600],   # March
    [161_300, 107_800, 124_500, 215_700],  # April
    [158_700, 112_300, 131_200, 208_900],  # May
]

months = ["January", "February", "March", "April", "May"]
regional_targets = [150_000, 100_000, 115_000, 200_000]


# --- Step 1: Compute regional totals using zip ---
# zip(*monthly_sales) transposes the 2D list from month-rows to region-columns.
regional_totals = {
    region: sum(figures)
    for region, figures in zip(regions, zip(*monthly_sales))
}

# --- Step 2: Monthly totals using enumerate and zip ---
monthly_totals = []
for month_num, (month, figures) in enumerate(zip(months, monthly_sales), start=1):
    monthly_totals.append((month_num, month, sum(figures)))

# --- Step 3: Find best and worst months using a loop ---
best_month_total = 0
best_month_name = ""
worst_month_total = float("inf")
worst_month_name = ""

for _, month, total in monthly_totals:
    if total > best_month_total:
        best_month_total = total
        best_month_name = month
    if total < worst_month_total:
        worst_month_total = total
        worst_month_name = month

# --- Step 4: Flag regions that missed target using list comprehension ---
num_months = len(months)
period_targets = [t * num_months for t in regional_targets]

below_target = [
    region
    for region, total, target in zip(regions, regional_totals.values(), period_targets)
    if total < target
]

# --- Step 5: Print the report ---

def separator(char="-", width=60):
    print(char * width)

separator("=")
print("  ACME CORP — MONTHLY REGIONAL SALES REPORT")
print(f"  Period: {months[0]}–{months[-1]}")
separator("=")

# Monthly totals with running cumulative
print("\nMONTHLY TOTALS")
separator()
running_total = 0
for month_num, month, total in monthly_totals:
    running_total += total
    print(
        f"  {month_num}. {month:<12} ${total:>10,.0f}"
        f"    Cumulative: ${running_total:>12,.0f}"
    )
separator()
print(f"  {'TOTAL':>14} ${running_total:>10,.0f}")

# Regional performance
print("\nREGIONAL PERFORMANCE vs. TARGET")
separator()
print(
    f"  {'Region':<14} {'Actual':>10} {'Target':>10} "
    f"{'Variance':>12} {'% vs Tgt':>10}"
)
separator()

for rank, (region, target) in enumerate(zip(regions, period_targets), start=1):
    total = regional_totals[region]
    variance = total - target
    pct = (variance / target) * 100
    flag = " *" if region in below_target else ""
    print(
        f"  {rank}. {region:<12} ${total:>9,.0f} ${target:>9,.0f}"
        f" ${variance:>+10,.0f}  {pct:>+7.1f}%{flag}"
    )

if below_target:
    print(f"\n  * Missed target: {', '.join(below_target)}")

# Nested loop: month-by-month breakdown per region
print("\nMONTH-BY-MONTH DETAIL BY REGION")
separator()
col = 11

header = f"  {'Region':<14}" + "".join(f"{m[:3]:>{col}}" for m in months)
print(header)
separator()

for region in regions:               # Outer loop: each region
    row = f"  {region:<14}"
    for month_figures in monthly_sales:  # Inner loop: each month's figures
        # Find this region's figure for this month
        region_index = regions.index(region)
        figure = month_figures[region_index]
        row += f"${figure:>{col - 1},.0f}"
    print(row)

# Best/worst summary
print(f"\nBEST MONTH:  {best_month_name} (${best_month_total:,.0f})")
print(f"WORST MONTH: {worst_month_name} (${worst_month_total:,.0f})")
separator("=")
print("  END OF REPORT")
separator("=")

Sample output:

============================================================
  ACME CORP — MONTHLY REGIONAL SALES REPORT
  Period: January–May
============================================================

MONTHLY TOTALS
------------------------------------------------------------
  1. January     $  543,500    Cumulative: $      543,500
  2. February    $  566,800    Cumulative: $    1,110,300
  3. March       $  544,600    Cumulative: $    1,654,900
  4. April       $  609,300    Cumulative: $    2,264,200
  5. May         $  611,100    Cumulative: $    2,875,300
------------------------------------------------------------
            TOTAL $2,875,300

REGIONAL PERFORMANCE vs. TARGET
------------------------------------------------------------
  Region          Actual     Target    Variance  % vs Tgt
------------------------------------------------------------
  1. Northeast    $756,500  $750,000    +$6,500    +0.9%
  2. Southeast    $513,400  $500,000   +$13,400    +2.7%
  3. Midwest      $596,500  $575,000   +$21,500    +3.7%
  4. West Coast $1,008,900$1,000,000    +$8,900    +0.9%

MONTH-BY-MONTH DETAIL BY REGION
------------------------------------------------------------
  Region            Jan        Feb        Mar        Apr        May
------------------------------------------------------------
  Northeast    $145,200   $152,800   $138,500   $161,300   $158,700
  Southeast     $98,400   $103,700    $91,200   $107,800   $112,300
  Midwest      $112,600   $108,900   $119,300   $124,500   $131,200
  West Coast   $187,300   $201,400   $195,600   $215,700   $208,900

BEST MONTH:  May ($611,100)
WORST MONTH: January ($543,500)
============================================================
  END OF REPORT
============================================================

This 80-line script produces a complete, professional report from raw data. Priya's version of this — adapted to pull data from a file (Chapter 9) and send the output by email (Chapter 19) — would replace the Monday morning process permanently.


5.12 Loop Performance: A Practical Note

For the datasets you are working with now — hundreds or a few thousand records — loop performance is not a concern. Modern Python executes millions of simple operations per second.

Where performance becomes relevant:

  • Very large datasets (100,000+ records): consider pandas (Chapter 10), which processes data in optimized C code rather than Python loops.
  • Deeply nested loops: a triple-nested loop over 1,000 items each runs a billion iterations. Flatten the data or find a better algorithm.
  • Repeated dictionary lookups in a loop: dictionaries are fast (O(1) lookup), but if you are building a dictionary inside a loop that itself is inside another loop, restructure the data first.

For now: write clear, correct code. Optimize when you have measured that speed is actually a problem.


Chapter Summary

Loops are the mechanism that transforms Python from a calculator into an automation engine. The core ideas from this chapter:

  • for loops iterate over any sequence — lists, ranges, strings, dictionary items.
  • while loops repeat until a condition is false, useful when you do not know in advance how many iterations you need.
  • range() generates integer sequences for numeric iteration patterns.
  • break exits a loop early when you have found what you need.
  • continue skips the current iteration without stopping the loop.
  • pass is a no-op placeholder for empty code blocks.
  • Nested loops handle multi-dimensional data; use them carefully at scale.
  • List comprehensions provide a concise, readable syntax for transforming and filtering sequences into new lists.
  • enumerate() gives you both index and value without manual counters.
  • zip() combines parallel sequences element by element.
  • The accumulator pattern (total = 0; for x in data: total += x) is the foundation of all aggregation.

The code in this chapter is the engine that will power every report, every analysis, and every automation tool you build throughout the rest of this book.


What's Next

Chapter 6 introduces functions — the mechanism for packaging your loop code into reusable, named units. The generate_weekly_summary function you saw in Case Study 1 is a preview of how functions transform a one-time script into a reusable tool. Once you can write loops and functions together, you have the core building blocks of any business application.


Key Terms

  • Iteration — The process of accessing each element in a sequence one at a time.
  • Loop — A control structure that repeats a block of code multiple times.
  • for loop — A loop that iterates over each element in a defined sequence.
  • while loop — A loop that repeats as long as a Boolean condition remains true.
  • range() — A built-in function that generates a sequence of integers.
  • break — A keyword that immediately exits the enclosing loop.
  • continue — A keyword that skips the rest of the current loop iteration and moves to the next.
  • pass — A keyword that does nothing; used as a placeholder in empty code blocks.
  • Nested loop — A loop contained within another loop's body.
  • List comprehension — A concise syntax for creating a new list by transforming or filtering an existing sequence.
  • enumerate() — A built-in function that adds a counter to an iterable, yielding (index, value) pairs.
  • zip() — A built-in function that combines multiple iterables element by element into tuples.
  • Accumulator pattern — The programming pattern of initializing a variable before a loop and updating it with each iteration.
  • Guard clause — A conditional check at the top of a loop body that uses continue (or break) to skip or exit early, reducing nesting.