Skip to content

Chapter 5: Loops and Repetition Structures

Updated: 2026-10-17 19:15 · English
Table of contents (21)

Navigation: Previous: Chapter 4 - if Statements and Branching Structures

Language: English | 中文版

In university administration and business data analysis, we constantly run into two kinds of repetitive work.

The first is repeating the same operation across many independent items. Suppose Southwestern University of Finance and Economics (SWUFE) needs to send an autumn sports meet opening notice to more than 20,000 enrolled students. The rule is identical for every recipient: read the student's name and ID, plug them into a standard message template, and send the notice. Notifying the second student does not depend on the first; the system simply carries out the same independent step more than 20,000 times.

The second is building up a running total step by step. Suppose the academic registry calculates a student's Grade Point Average (GPA) from a transcript that lists several courses, each with its credits and grade points:

\text{GPA} = \frac{\sum (\text{Course Credits} \times \text{Course Grade Points})}{\sum \text{Course Credits}}

To compute this ratio, each course requires two additions: we add \text{Course Credits} \times \text{Course Grade Points} to the numerator (total grade points), and we add \text{Course Credits} to the denominator (total credits). When processing the second course, the system adds those amounts onto the subtotals from the first course. Each step updates the running total from the step before it.

If we write only top-to-bottom sequential code, both tasks quickly hit a wall:

  • Code length grows with data size: In strictly sequential code, every action needs its own line. Sending notices to 20,000 students would require 20,000 lines of code. If the wording of the notice changes, someone has to edit all 20,000 lines by hand.
  • Fixed code cannot adapt to changing data counts: One student might take 5 courses this semester and 8 next semester. A script with a fixed number of lines cannot automatically adjust to transcripts of different lengths.

To solve both problems, we use a loop structure. The core idea of a loop is to separate the changing data from the fixed processing rule: we write the code for a single step once, and let the computer repeat that step for every item or until a stopping condition is met.

  • For batch tasks (such as sending mass notices): Write the statement that notifies one student, and let the loop repeat it across the entire roster. Whether there are 3 students or 20,000, the code stays two or three lines long.
  • For running totals (such as calculating GPA): Write the rule that adds one course to the running totals, and let the loop apply that rule to each course on the transcript.

Python provides two loop statements for these two situations:

  • The for loop: Use for when you know the sequence or number of items in advance—such as stepping through a roster to send notices or reading through a transcript to sum credits.
  • The while loop: Use while when you do not know the number of repetitions beforehand and want to keep repeating as long as a condition holds—such as adding monthly savings until an account balance reaches a target.

In Chapter 4, branching structures (if / elif / else) let our programs choose which path to take. In this chapter, loop structures (for and while) let our programs control how many times a set of steps runs. Together, branching and loops can express almost any business rule or computational workflow.

This chapter is organized in two parts:

  • Core Concepts Part I covers the for loop with range(), accumulators and counters, condition-driven repetition with the while loop, altering loop flow with break and continue, and packaging loops inside functions.
  • Core Concepts Part II introduces lists for storing business records, iterating over lists, nested loops for two-dimensional grid analysis, pairing sequence numbers with items via enumerate(), the loop else clause, and interactive data entry using sentinel values.

Core Concepts Part I

5.1 Repeating the Same Steps: The for Loop

5.1.1 From Repetitive Code to Repeated Execution

Let us start with a small version of the notification problem: sending a sports meet notice to three students with IDs 1, 2, and 3. Without a loop, we would write three separate print() statements:

python
print("Sending sports meet notice to student 1")
print("Sending sports meet notice to student 2")
print("Sending sports meet notice to student 3")

Output:

text
Sending sports meet notice to student 1
Sending sports meet notice to student 2
Sending sports meet notice to student 3

Look closely at those three lines: the action and wording are identical, and only the student ID (1, 2, 3) changes from one line to the next.

Whenever the action stays the same and only the value changes, we can separate the changing value from the fixed action. We introduce a loop variable student_id that takes the values 1, 2, and 3 one after another, and run the print() statement once for each value:

python
# For each student_id from 1 up to (but not including) 4:
#     Send the sports meet notice to student_id
# Once all notices are sent, print the completion message
for student_id in range(1, 4):
    print(f"Sending sports meet notice to student {student_id}")

print("All notices sent.")

Output:

text
Sending sports meet notice to student 1
Sending sports meet notice to student 2
Sending sports meet notice to student 3
All notices sent.

Here, range(1, 4) produces a sequence of consecutive integers starting at 1 and stopping just before 4—namely 1, 2, and 3. Because range() follows the "include start, exclude stop" rule, 4 is not included (we examine range() bounds and step sizes in Section 5.2).

When Python runs for student_id in range(1, 4):, it works like an assembly line:

  1. It takes the next integer from range(1, 4) and assigns it to the loop variable student_id.
  2. It runs the indented block of code below the header (the loop body).
  3. As soon as the loop body finishes, Python automatically goes back to range(1, 4), assigns the next integer to student_id, and runs the loop body again—no manual counter update needed.

for loop execution animation

Each complete pass through the loop body for one item in the sequence is called an iteration. Because range(1, 4) contains 3 integers, this loop runs 3 iterations. Once all three numbers have been processed, the loop ends automatically, and Python moves on to the first unindented line below the loop: print("All notices sent."). If we change range(1, 4) to range(7, 10), student_id takes 7, 8, and 9 in turn.

Iteration mechanism in Python loops

Tip (Loop Variable Naming): student_id is a variable name you choose, not a Python keyword. If you rename it in the for header, update its name inside the loop body to match. Pick clear nouns that describe what each item represents (such as student_id, month, or price) so the loop reads naturally.

5.1.2 The Basic Structure of a for Loop

A for loop paired with range() has the following structure:

text
for loop_variable in range(start, stop):
    loop_body_statement_1
    loop_body_statement_2
    ...

statement_executed_after_loop_ends

Here, range(start, stop) supplies an ordered sequence of integers, and loop_variable receives the current integer at the start of each iteration.

The keywords for and in work as a pair, and the colon : at the end of the header introduces the indented loop body. Every statement indented by 4 spaces beneath for belongs to the loop body and repeats on each iteration. As soon as a statement returns to the same indentation level as for, it sits outside the loop and runs only once, after the entire loop has finished.

In-Class Exercise: Change range(1, 4) to range(1, 5) and then to range(7, 10). Write down on paper which student IDs will print and how many lines of output you expect, then run the code to check.


5.2 Repeating Across an Integer Sequence: range()

5.2.1 Start, Stop, and the Half-Open Interval Principle

In Section 5.1, range(1, 4) gave us 1, 2, and 3. What if we want the integers from 1 through 5? We set the stop value to 6—one past the last number we want:

python
for number in range(1, 6):
    print(number)

Output:

text
1
2
3
4
5

The call range(1, 6) generates integers starting at 1 and stopping just before 6. In mathematics, this is the half-open interval [\text{start}, \text{stop}): include the start, exclude the stop.

Why does Python exclude the stop value instead of including it? Two practical benefits show up right away:

  1. The number of items is simply \text{stop} - \text{start}: You never have to add 1 to find the length. For example, range(1, 6) has 6 - 1 = 5 numbers; looping over 12 months with range(1, 13) runs 13 - 1 = 12 times; looping from 1 through n with range(1, n + 1) runs (n + 1) - 1 = n times.
  2. Adjacent ranges join cleanly without overlap: If you split a workload into two consecutive batches—range(1, 5) and range(5, 9)—the first batch ends at 4 and the second starts at 5. The boundary value 5 is neither counted twice nor left out.

Whenever you step forward by 1 and want your loop to include the upper bound n, set the stop argument to n + 1.

Problem: Write a function print_triangle(n: int) -> None that prints a right-angled triangle of asterisks (*) with n rows.

When n = 4, the output should be:

text
*
**
***
****

Analysis: Row 1 has 1 asterisk, row 2 has 2 asterisks, and in general row i has i asterisks. That means our row index i needs to run through 1, 2, \dots, n. Because range() excludes its stop value, we write range(1, n + 1) so that row n is included:

python
def print_triangle(n: int) -> None:
    for i in range(1, n + 1):
        print("*" * i)

# Function call
print_triangle(4)

5.2.2 Start, Stop, and Step

We can call range() with one, two, or three integer arguments:

Syntax Form Rule Example Generated Sequence
range(stop) Stop only: starts at 0 by default, steps by +1 range(4) 0, 1, 2, 3
range(start, stop) Start and stop: steps by +1 by default, excludes stop range(1, 4) 1, 2, 3
range(start, stop, step) Start, stop, and step: adds step each time, excludes stop range(1, 7, 2) 1, 3, 5

When called with a single argument range(stop), Python starts at 0 and produces stop integers (0 through stop - 1).

When called with three arguments, the third argument is the step size—the amount added to move from one number to the next. A positive step counts upward; a negative step counts downward. For instance, to print a 3-day countdown to the sports meet, we start at 3, stop before 0, and step by -1:

python
for remaining_days in range(3, 0, -1):
    print(f"Days until opening: {remaining_days}")

Output:

text
Days until opening: 3
Days until opening: 2
Days until opening: 1

Starting at 3 and adding -1 each time produces 3, 2, and 1, stopping right before 0.

Tip (Argument Constraints for range): All arguments to range() must be integers. Passing a float (such as range(1, 4, 0.5)) raises a TypeError. Passing a step size of 0 raises a ValueError because a step of zero would never move toward the stop bound.

5.2.3 Boundaries and Empty Sequences

A for loop body runs only if range() produces at least one integer. What happens if start already sits at or beyond stop, or if step points away from stop? Then range() produces an empty sequence, and the loop body runs 0 times:

Expression Generated Integers Loop Body Iterations Reason
range(1, 4) 1, 2, 3 3 start < stop; stepping by +1 moves from 1 toward 4
range(4) 0, 1, 2, 3 4 Starts at 0 by default and steps by +1, producing 4 integers
range(3, 0, -1) 3, 2, 1 3 start > stop; stepping by -1 moves from 3 down toward 0
range(3, 3) (none) 0 start == stop; excluding 3 leaves no integers in [3, 3)
range(3, 0) (none) 0 Default step is +1; stepping upward from 3 moves away from 0 at once

In short: with a positive step, numbers are generated only while < stop; with a negative step, numbers are generated only while > stop. When range() is empty, Python does not raise an error—it simply skips the loop body and continues with the code below the loop.

In-Class Exercise: Use range() to print the month numbers for the first half of the year (1 through 6), and then write a second loop to print only the even months (2, 4, and 6). Write down your range() arguments on paper first, then run the code to verify that Month 6 is included in both outputs.


5.3 Accumulation and Counting in Loops

5.3.1 Summing from 1 to N: The Accumulator

Writing 1 + 2 + 3 + 4 + 5 by hand works for five numbers, but not for summing the integers from 1 to 100 or totaling thousands of sales records.

How do we build a sum inside a for loop? Notice that the loop variable number is overwritten by the next integer on every iteration, so number alone cannot remember earlier values. To keep a running sum across iterations, we define a separate variable total = 0 before the loop starts, and add each new number into total inside the loop body. A variable that builds up a cumulative result across loop iterations is called an accumulator.

python
n = 5
total = 0  # Initialize accumulator: total starts at 0 before summing begins

# For each integer from 1 to n:
#     Add number to total
for number in range(1, n + 1):
    total = total + number

# Loop finished; resume main flow
print(f"Sum of integers from 1 to {n}: {total}")

Output:

text
Sum of integers from 1 to 5: 15

Because we want to include n in the sum, we write range(1, n + 1).

Inside the loop, the assignment total = total + number works in two steps: first evaluate the right-hand side total + number using the current value of total, and then store that new sum back into total on the left-hand side. In accounting terms, this is simply \text{Ending Balance} = \text{Beginning Balance} + \text{Current Transaction}:

Stage Current number total Before Addition Formula and Assignment total After Addition
Init — — Initial ledger set to 0 0
Iteration 1 1 0 0 + 1 1
Iteration 2 2 1 1 + 2 3
Iteration 3 3 3 3 + 3 6
Iteration 4 4 6 6 + 4 10
Iteration 5 5 10 10 + 5 15

At the end of each iteration, total holds the sum of all integers from 1 up to the current number. After the fifth iteration completes, total holds 15.

Tip (Augmented Assignment): Python provides a shorthand for updating a variable in place: total = total + number can be written as total += number. Likewise, adding 1 to a counter is usually written as count += 1.

5.3.2 Where to Initialize and Where to Output

What happens if we accidentally place total = 0 inside the loop body? Then every iteration resets total back to 0 right before adding number, wiping out everything accumulated in earlier iterations:

python
# Erroneous example: resetting the accumulator inside the loop discards previous progress
n = 5

for number in range(1, n + 1):
    total = 0  # Bug: total resets to 0 on every iteration
    total = total + number

print(f"Incorrect total: {total}")

Output:

text
Incorrect total: 5

Because Python raises no syntax error here, this is a classic logic bug: the final output simply reflects the last iteration (0 + 5 = 5). Always initialize accumulators before the loop header.

Similarly, where you indent print() controls what kind of report your program produces:

  • Indented inside the loop body: print() runs on every iteration, showing the running subtotal after each step (like a detailed transaction log).
  • Unindented after the loop body: print() runs once after all iterations finish, reporting only the final sum (like a period-end summary).

5.3.3 Combining Loops and Conditional Statements

So far, our loop has added every number in the sequence. In practice, we often want to inspect a sequence of records and count only those that pass a specific test.

Problem: Inspect the integers from 1 through 10, print each number that is divisible by 3, and count how many such numbers exist.

In Python, the modulo operator % gives the remainder after integer division, so number is divisible by 3 whenever number % 3 == 0. By nesting an if statement inside the for loop, we print and count number only when that condition holds:

python
n = 10
count = 0  # Initialize match counter to 0

# For each number from 1 to n:
#     If number is divisible by 3:
#         Print number and increment counter
for number in range(1, n + 1):
    if number % 3 == 0:
        print(number)
        count = count + 1  # Or count += 1

print(f"Numbers divisible by 3: {count}")

Output:

text
3
6
9
Numbers divisible by 3: 3

Here, the for loop steps through all 10 integers (1 to 10), while the inner if statement acts as a filter. The indented print(number) and count = count + 1 statements run only when number is 3, 6, or 9.

Pay close attention to the indentation of count = count + 1:

  • Indented inside the if block (8 spaces): Increments count only when number % 3 == 0 holds, counting matching items (3).
  • Aligned with if inside the for block (4 spaces): Increments count on every iteration regardless of divisibility, counting all items inspected (10).

5.3.4 Application: Accumulating Monthly Spending Increases

Let us put sequence generation, formula calculation, and accumulation together in a departmental budgeting problem. Suppose a department plans a 3-month quarterly budget: Month 1 spending is 1200 yuan, and planned spending rises by 100 yuan each month after that (1200, 1300, and 1400 yuan).

How does planned spending depend on the month number month? Month 1 adds 0 \times 100 to the 1200-yuan base, Month 2 adds 1 \times 100, and Month 3 adds 2 \times 100. In general, for any month month:

\text{Month } \text{month} \text{ Spending} = 1200 + (\text{month} - 1) \times 100

Inside the loop, we compute monthly_cost from this formula, add it to total_cost, and print the monthly line item:

python
month_count = 3
total_cost = 0

for month in range(1, month_count + 1):
    monthly_cost = 1200 + (month - 1) * 100
    total_cost = total_cost + monthly_cost
    print(f"Month {month}: planned spending {monthly_cost} yuan")

print(f"Total planned spending: {total_cost} yuan")

if month_count > 0:
    average_cost = total_cost / month_count
    print(f"Average monthly spending: {average_cost:.2f} yuan")
else:
    print("No budget months; cannot calculate average spending")

Output:

text
Month 1: planned spending 1200 yuan
Month 2: planned spending 1300 yuan
Month 3: planned spending 1400 yuan
Total planned spending: 3900 yuan
Average monthly spending: 1300.00 yuan

Let us trace how the variables work together across the three parts of the script:

  1. Before the loop: We set month_count = 3 and initialize the accumulator total_cost = 0.
  2. Inside the loop: On each iteration, monthly_cost computes that single month's budget (1200, then 1300, then 1400), while total_cost accumulates the running sum (1200, then 2500, then 3900). Notice the distinct roles of these two variables: monthly_cost is overwritten each month, whereas total_cost carries the cumulative sum forward across months.
  3. After the loop: We print total_cost (3900 yuan) and check if month_count > 0: before dividing total_cost / month_count. If month_count were 0, range(1, 1) would be empty, the loop would run 0 times, and the if month_count > 0: guard would prevent a ZeroDivisionError.

In-Class Exercise: Test the summation script from Section 5.3.1 with n = 1, 10, 0, and test the divisibility counter from Section 5.3.3 with n = 2, 3, 12. Write down the expected output on paper first, then run your code to check the boundary cases.

Extended Approach: Storing in a List Before Aggregating

In the script above, each month's monthly_cost is overwritten on the next iteration. What if a later part of our program still needs the individual monthly figures after the loop ends? Then we split the work into two stages: first collect each month's figure in a list, and then loop over the list to compute the total.

A list is Python's standard container for an ordered sequence of values:

  • Syntax: A list writes its items inside square brackets [], separated by commas (such as [1200, 1300, 1400]).
  • Appending: We start with an empty list monthly_costs = [] and call monthly_costs.append(cost) on each iteration to add the new month's cost to the end of the list (we cover lists systematically in Section 5.7).
python
month_count = 3
monthly_costs = []  # Empty list to store monthly costs

# Stage 1: Compute monthly budget and store in list
for month in range(1, month_count + 1):
    cost = 1200 + (month - 1) * 100
    monthly_costs.append(cost)
    print(f"Month {month}: planned spending {cost} yuan")

# Stage 2: Iterate directly over list elements to sum
total_cost = 0
for cost in monthly_costs:
    total_cost += cost

print(f"Total planned spending: {total_cost} yuan")

if month_count > 0:
    average_cost = total_cost / month_count
    print(f"Average monthly spending: {average_cost:.2f} yuan")
else:
    print("No budget months; cannot calculate average spending")

The output is identical to the single-loop version, but monthly_costs now retains [1200, 1300, 1400] in memory for any follow-up analysis. Notice how Stage 2 writes for cost in monthly_costs: to step directly through the stored numbers without needing range(). Separating data collection from summary calculation is a clean, reusable pattern that we will use throughout Part II.


5.4 Execute While a Condition Holds: The while Loop

5.4.1 When Repetition Count is Unknown: The while Loop

A for loop works well whenever you know the sequence or the exact number of iterations before the loop starts. In many financial and business problems, however, you do not know in advance how many steps a process will take.

Suppose a startup team is saving toward a 2500-yuan reserve target. Why can't we easily write this with for month in range(...)? Because we do not know what stop value to pass to range(). Pick a stop value that is too small, and the loop quits before the reserve reaches 2500 yuan; pick one that is too large, and the loop keeps running unneeded months after the target is met. Here, when to stop depends on the running balance, not on a preset number of months. Whenever the number of repetitions is unknown beforehand and depends on a condition that changes during execution, we use a while loop.

Problem: A startup project begins with 1000 yuan in seed capital. At the end of each month, the team adds 600 yuan (assuming zero expenses and zero interest). How many full months will it take for the reserve to reach or exceed 2500 yuan?

Let us trace the balance by hand first: Month 1 ends with 1600 yuan, Month 2 ends with 2200 yuan, and Month 3 ends with 2800 yuan—which reaches and passes 2500 yuan. Stated as a loop rule: as long as the reserve is strictly below 2500 yuan, add another month of funding; as soon as the reserve reaches 2500 yuan or more, stop.

python
reserve = 1000
target = 2500
monthly_addition = 600
months = 0  # Initialize month counter to 0

# While reserve is strictly less than target:
#     Add one month of funding and increment months counter
while reserve < target:
    reserve += monthly_addition
    months = months + 1
    print(f"End of month {months}: balance {reserve:.2f} yuan")

print(f"Target reached in {months} months")

Output:

text
End of month 1: balance 1600.00 yuan
End of month 2: balance 2200.00 yuan
End of month 3: balance 2800.00 yuan
Target reached in 3 months

The header while reserve < target: tells Python: as long as reserve < target evaluates to True, run the indented loop body. Each pass through the body adds 600 to reserve and adds 1 to months. Once the body finishes, Python jumps back to the while header and tests reserve < target again against the updated balance.

5.4.2 Testing Conditions Before Running the Loop Body

A while loop has the following general structure:

text
initialize_state_variables

while condition_expression:
    statements_to_repeat_while_true
    update_state_toward_termination

statements_executed_after_loop_ends

Here, condition_expression is a boolean expression that evaluates to True or False. The table below traces every condition check and state update in the startup funding script:

Check Number reserve at Check Condition reserve < 2500 Enter Loop Body? Balance and Months After Iteration
Check 1 1000 yuan True Yes 1600 yuan, 1 month
Check 2 1600 yuan True Yes 2200 yuan, 2 months
Check 3 2200 yuan True Yes 2800 yuan, 3 months
Check 4 2800 yuan False No Remains 2800 yuan, 3 months; exits

Notice two important facts from this trace:

  1. Python checks the condition before every iteration: Even before the very first pass, Python tests reserve < 2500. If the startup began with reserve = 3000, Check 1 would immediately evaluate to False, the loop body would run 0 times, and the script would report 0 months.
  2. The condition is checked one more time than the body runs: Here the loop body runs 3 times, but reserve < 2500 is tested 4 times. When reserve reaches 2800 inside the third iteration, the loop does not stop mid-body; it finishes printing Month 3, returns to the while header for Check 4, sees 2800 < 2500 is False, and exits cleanly.

5.4.3 Advancing the Loop and Preventing Infinite Loops

Every working while loop relies on three coordinated parts: initialize the state variables before the loop, test the continuation condition in the while header, and update the state inside the loop body so it moves closer to stopping.

In our funding example, reserve starts at 1000, grows by 600 on each iteration, and continues while reserve < 2500. Because reserve increases by a positive amount each month, it is guaranteed to cross 2500 after a finite number of steps, turning reserve < 2500 to False.

If any part of that update chain is broken, the condition never becomes False:

Common Bug Balance Behavior Runtime Consequence
Omitting reserve += monthly_addition Stays frozen at 1000 yuan Condition 1000 < 2500 stays True forever
Setting monthly_addition = 0 Adds 0 each round; balance unchanged Reserve stays at 1000 yuan and never reaches 2500 yuan
Accidentally using negative addition Balance shrinks each month Reserve moves further below 2500; condition stays True
Writing continuation as reserve != target Balance jumps from 2200 to 2800 yuan Balance steps right past 2500; reserve != 2500 stays True forever

A loop whose continuation condition never becomes False is called an infinite loop. If your program hangs or prints lines endlessly, click the Stop button in your IDE or press Ctrl + C in the terminal to interrupt it, then check how your state variable changes inside the loop body.

Look closely at the fourth row of the table: writing while reserve != target: instead of while reserve < target:. Because reserve jumps in steps of 600 yuan—from 1600 to 2200 to 2800—it never lands on 2500 exactly. Testing reserve != 2500 misses the crossing and keeps adding money forever. Whenever a loop steps toward a threshold, test the inequality (reserve < target), not exact inequality (reserve != target).

Tip (Currency Units and Floating-Point Precision): To keep the focus on loop mechanics, these examples use integer yuan. In financial software, you should also avoid testing exact equality on float values because of binary rounding; see Tip 2 in the Supplementary Notes.

5.4.4 Choosing Between for and while Loops

Since both for and while repeat a block of code, how should you choose between them when designing a program? Ask one question: Do you know the sequence or the number of repetitions before the loop begins?

Typical Business Task Recommended Loop Selection Rationale
Sum integers from 1 to N for with range() Start and stop bounds are known; direct sequence traversal is cleanest
Process 12 monthly budget rows in order for with range() Fixed at 12 iterations; sequence moves by month index
Add monthly funds until target is reached while Iteration count is unknown beforehand; depends on running balance
Scan cashier barcode inputs until checkout while Item count is unknown beforehand; ends on specific checkout command

Of course, any for loop over a known range can be rewritten as a while loop. For example, here is how we sum the integers from 1 to n using while:

python
n = 5
total = 0
number = 1

while number <= n:
    total += number
    number += 1

print(f"Sum of integers from 1 to {n}: {total}")

Output:

text
Sum of integers from 1 to 5: 15

Comparing this while version with for number in range(1, n + 1): shows why for is cleaner when the range is known. The for loop advances number automatically, whereas the while loop requires you to write three separate lines of bookkeeping by hand:

Stage Code Purpose
Initialize number = 1 Set the starting integer before the loop
Test bound number <= n Keep looping while number is at most n (so n itself is included)
Step increment number += 1 Advance number by 1 at the end of each iteration

If you forget the line number += 1 at the bottom of the while body, number stays stuck at 1 and the program enters an infinite loop. Whenever the sequence or iteration count is known in advance, use a for loop: it keeps all range bookkeeping in the header and cannot forget to advance the loop variable.

In-Class Exercise: Test the startup funding script from Section 5.4.1 with target set to 2200, 2500, and 1000 yuan. Work out the number of months on paper first, then run the script to verify. Notice how these three cases test hitting the target exactly, stepping past the target, and starting at the target.


5.5 Changing Loop Flow: break and continue

5.5.1 Exiting Early After Finding a Target: break

Up to now, every loop has run until its sequence was exhausted or its while header condition turned False. When searching for a target value, however, we often want to stop the loop the moment we find the first match—without wasting time checking the remaining candidates.

Python's break statement does exactly that: it exits the current loop immediately. As soon as Python executes break, it skips the rest of the loop body, leaves the loop altogether, and continues at the first unindented statement below the loop.

Problem (Target Profit Planning): A product sells for 50 yuan per unit, has a variable cost of 30 yuan per unit, and carries 1000 yuan in fixed costs. Assuming every unit produced is sold, find the smallest integer sales volume between 1 and 200 units that earns a profit of at least 1000 yuan.

From cost-volume-profit (CVP) analysis, profit is:

\text{Profit} = (\text{Unit Price} - \text{Unit Cost}) \times \text{Sales Volume} - \text{Fixed Cost}

Because each additional unit sold adds 50 - 30 = 20 yuan to profit, profit rises steadily with sales volume. If we test candidate quantities in ascending order (1, 2, 3, ...), the first quantity that achieves \text{profit} \ge 1000 is guaranteed to be the minimum qualifying volume. Once we find it, we print the answer and exit the loop with break:

python
unit_price = 50
unit_cost = 30
fixed_cost = 1000
target_profit = 1000
found = False  # Initialize flag variable: False indicates target not yet found

# For each candidate quantity from 1 to 200:
#     Calculate profit
#     If profit meets or exceeds target_profit:
#         Record match, print result, and break out of loop immediately
for quantity in range(1, 201):
    profit = (unit_price - unit_cost) * quantity - fixed_cost
    if profit >= target_profit:
        found = True
        print(f"First qualifying sales volume: {quantity} units")
        print(f"Corresponding profit: {profit} yuan")
        break

if not found:
    print("No qualifying sales volume found between 1 and 200 units")

print("Search finished")

Output:

text
First qualifying sales volume: 100 units
Corresponding profit: 1000 yuan
Search finished

Let us trace what happens: for quantities 1 through 99, profit remains below 1000 yuan (at 99 units, profit is (50 - 30) \times 99 - 1000 = 980 yuan). On iteration 100, profit reaches 1000 yuan. The if profit >= target_profit: check succeeds, sets found = True, prints the result, and runs break—skipping quantities 101 through 200 completely.

Why do we need the boolean variable found? Once execution reaches the lines below the loop, the program needs to know how the loop ended: did it exit early via break because a solution was found, or did it test all 200 quantities without ever reaching target_profit? A boolean variable that remembers whether an event occurred during a loop is called a flag variable: we set found = False before the loop, flip it to True inside the if branch when a match appears, and check if not found: after the loop to print a notice when no candidate qualified (try setting target_profit = 5000 to see this branch run).

5.5.2 Skipping the Current Iteration: continue

Sometimes we do not want to stop the entire loop when we hit a special record; we just want to skip the rest of the current iteration and move straight on to the next item. That is the job of the continue statement: when Python executes continue, it skips the remaining lines in the loop body for the current iteration and jumps directly to the next iteration.

Problem: A trade fair runs for 7 days (Days 1 through 7), but the venue closes on Day 3 for scheduled maintenance. Each open day costs 80 yuan in venue maintenance fees, while closed days cost nothing. Print each open day, and report the total number of open days and the total maintenance fee.

python
closed_day = 3
daily_cost = 80
open_days = 0
total_cost = 0

# For each day from 1 to 7:
#     If day is the closed day, skip the rest of this iteration
#     Otherwise, increment open days, add maintenance fee, and print open notice
for day in range(1, 8):
    if day == closed_day:
        continue

    open_days += 1
    total_cost += daily_cost
    print(f"Day {day}: venue open")

print(f"Open days: {open_days}")
print(f"Total maintenance fee: {total_cost} yuan")

Output:

text
Day 1: venue open
Day 2: venue open
Day 4: venue open
Day 5: venue open
Day 6: venue open
Day 7: venue open
Open days: 6
Total maintenance fee: 480 yuan

When day is 3, day == closed_day is True. Python executes continue, which skips open_days += 1, total_cost += daily_cost, and print(...) for Day 3, and moves straight on to Day 4.

Contrast continue with break here: if we accidentally wrote break instead of continue on Day 3, the loop would stop altogether on Day 3 and never process Days 4 through 7.

Could we have written this without continue by placing the three update lines inside if day != closed_day:? Yes. Using if day == closed_day: continue at the top of the loop is known as a guard clause: it filters out exceptions right away so the normal business logic below stays flat without an extra level of indentation.

5.5.3 Differences in continue Behavior Between for and while Loops

When you use continue, keep in mind an important difference between for and while loops:

  • In a for loop, continue jumps back to the for header, which automatically fetches the next item from range() or the list.
  • In a while loop, continue jumps back to the while header to re-test the condition—it does not automatically increment your counter. If your counter update (record_no += 1) sits at the bottom of the loop body below continue, then continue will skip the counter update too, leaving the counter stuck at the same value forever!

For example, if we inspect records 1 through 4 with a while loop and want to skip record 2, we must update record_no before the continue check:

python
record_no = 0

while record_no < 4:
    record_no += 1  # Increment first so continue does not trap the counter
    if record_no == 2:
        continue
    print(f"Checking record {record_no}")

Output:

text
Checking record 1
Checking record 3
Checking record 4

What would happen if we started at record_no = 1 and put record_no += 1 at the very bottom of the while body? Iteration 1 would print record 1 and increment record_no to 2. On iteration 2, if record_no == 2: would run continue, jumping back to while record_no < 4: before reaching record_no += 1. Now record_no is still 2, so the next iteration hits continue again, trapping the program in an infinite loop. This is another reason why for loops are much less error-prone whenever you are stepping through a known sequence.

5.5.4 Distinguishing continue, break, and return

Let us put continue and break side by side with the return statement from Chapter 2 to see how their stopping scopes differ:

Statement What It Stops Where Execution Goes Next Typical Business Use Case
continue Current iteration only Jumps to the loop header to start the next iteration Skip a closed day, a missing value, or an invalid input
break Innermost enclosing loop Jumps to the first statement below that loop Found the first qualifying item; stop searching the rest
return Entire enclosing function Exits the function and hands the return value back Function has its final answer; return it to the caller

Remember their syntax rules: break and continue can appear only inside a loop (for or while), whereas return can appear only inside a function (def). When a loop sits inside a function, break leaves the loop but keeps running the rest of the function; return exits both the loop and the function immediately, no matter how many loops are nested around it.

In-Class Exercise: In the trade fair script from Section 5.5.2, change closed_day to 1 and then to 7. For each value, write down on paper what the script outputs with continue versus with break, then run the code to verify.


5.6 Organizing Loops in Functions

5.6.1 Parameterizing Computation Bounds

In Section 5.3.1, our script summed the integers from 1 to 5 by setting n = 5 at the top of the file. If we want to sum from 1 to 5, then from 1 to 100, and then from 1 to 0, we do not want to copy and paste the loop three times. Instead, we package the loop inside a function sum_integers(n: int) -> int and pass the upper bound n as a parameter:

python
def sum_integers(n: int) -> int:
    """Return the sum of integers from 1 to n, where n is a non-negative integer."""
    total = 0
    for number in range(1, n + 1):
        total += number
    return total


print(sum_integers(5))
print(sum_integers(100))
print(sum_integers(0))

Output:

text
15
5050
0

Each call to sum_integers(n) creates a fresh local variable total = 0, runs the loop for that specific argument n, and returns the final total. Because total is local to each call, calling sum_integers(100) after sum_integers(5) starts cleanly at 0 rather than adding onto 15. And when n = 0, range(1, 1) is empty, the loop body runs 0 times, and the function immediately returns the initial value total = 0.

Tip (Summation Formulas vs. Loops): Of course, the sum 1 + 2 + \dots + n can be computed in one step with Gauss's formula n * (n + 1) // 2. We write the loop out here to show the standard three-step function template that works for any series or dataset: initialize the accumulator \rightarrow update it inside the loop \rightarrow return the final result after the loop.

5.6.2 Returning Totals Only After Loop Completion

Look at the indentation of return total in sum_integers(): it sits at 4 spaces, aligned with for—outside the loop body. That alignment guarantees that the function waits until all iterations finish before returning total.

What happens if you accidentally indent return total inside the loop body (at 8 spaces)?

python
# Erroneous example: placing return inside the loop exits during iteration 1
def incorrect_sum(n: int) -> int:
    total = 0
    for number in range(1, n + 1):
        total += number
        return total  # Bug: returns immediately after adding 1 in iteration 1


print(incorrect_sum(5))
print(incorrect_sum(0))

Output:

text
1
None

Because return terminates the entire function immediately:

  • When we call incorrect_sum(5), iteration 1 sets number = 1, adds 1 to total, and hits return total on the very next line—exiting the function right away with 1 and never reaching 2, 3, 4, 5.
  • Even worse, when we call incorrect_sum(0), range(1, 1) is empty, so the loop body never runs at all! Execution skips the loop, reaches the end of the function without hitting any return statement, and returns None by default.

Keep this simple rule in mind whenever a function contains a loop:

  • When computing a total, average, or count across all items: Place return outside the loop (aligned with for or while) so every item is processed first.
  • When searching for the first item that matches a condition: Place return inside the if branch in the loop to return the match as soon as it is found—and still place a fallback return outside the loop in case no item matches (as we will do in Part II, Exercise 2).

5.6.3 Updating State in Sequence: The Fibonacci Sequence

An accumulator (total) or counter (count) only needs to remember one number from the previous step. Some problems, however, require remembering two previous numbers at each step.

A classic example is the Fibonacci sequence: Term 0 is 0, Term 1 is 1, and every term from Term 2 onward is the sum of the two terms immediately before it:

text
Index: 0  1  2  3  4  5  6   7
Value: 0  1  1  2  3  5  8  13

Notice that to compute Term 6 (8), we only need Term 4 (3) and Term 5 (5); we no longer need Terms 0 through 3. That means we do not have to store the entire sequence in memory. Instead, we can keep two state variables that act like a two-slot sliding window, shifting one step to the right on each iteration so they always hold the latest two adjacent terms.

Let previous hold the earlier term in the window (starting at Term 0: 0) and current hold the later term (starting at Term 1: 1). To slide the window one step to the right:

  1. First compute the sum of the two current window values: next_value = previous + current.
  2. Shift the window right by setting previous = current and current = next_value:
python
def fibonacci(n: int) -> int:
    """Return the n-th Fibonacci number, where n >= 0 and Term 0 is 0."""
    previous = 0
    current = 1

    # Shift state n times:
    #     Calculate next_value, then shift previous and current one step to the right
    for step in range(n):
        next_value = previous + current
        previous = current
        current = next_value

    return previous


print(fibonacci(0))
print(fibonacci(1))
print(fibonacci(2))
print(fibonacci(7))

Output:

text
0
1
1
13

Why does fibonacci(n) return previous at the end rather than current? Let us trace what previous and current hold after each shift:

Completed Shifts Value of previous Value of current Corresponding Sequence Terms
0 (start) 0 1 Term 0, Term 1
1 1 1 Term 1, Term 2
2 1 2 Term 2, Term 3
3 2 3 Term 3, Term 4
4 3 5 Term 4, Term 5
5 5 8 Term 5, Term 6

Before the loop runs (0 shifts), previous holds Term 0 (0). Each iteration shifts both variables forward by one term index. After n shifts, previous holds Term n and current holds Term n + 1. Returning previous therefore gives Term n for every n \ge 0—including the boundary case n = 0, where range(0) runs 0 shifts and the function immediately returns previous = 0.

Pay special attention to the order of the three assignments inside the loop: we save previous + current in the temporary variable next_value before we overwrite previous = current. If you leave out next_value and write previous = current first, you destroy the old value of previous before adding it (you will diagnose and fix this exact bug in Exercise 4 below).

In-Class Exercise: Extend the trace table above through Shift 6 and Shift 8 by hand. Then call fibonacci(6) and fibonacci(8) to verify your answers, and check what current holds at the end of each call.


Core Concepts Part I Exercises

These four exercises bring together arithmetic formulas, if branches, functions, and for / while loops (without lists). Before writing code for each problem, ask yourself three questions: What state variables do I need to initialize before the loop? How should each iteration update them? What condition stops the loop?

Exercise 1: Sum of Odd Integers from 1 to N

Write a function sum_odd_numbers(n: int) -> int that takes a non-negative integer n and returns the sum of all odd integers from 1 through n (including n if n is odd; returning 0 when n = 0).

Hint: You can either step through odd integers directly with range(1, n + 1, 2) or check if number % 2 != 0: inside range(1, n + 1).

Input n Expected Return Calculation Detail
0 0 No odd numbers in range; returns 0
1 1 Single odd number: 1
6 9 Summing odds: 1 + 3 + 5 = 9
7 16 Includes bound: 1 + 3 + 5 + 7 = 16

Exercise 2: Minimum Months to Reach Investment Target

Write a function months_to_target(initial: int, monthly: int, target: int) -> int that takes an initial balance initial, a fixed month-end contribution monthly, and a target balance target, and returns the minimum number of full months needed for the balance to reach or exceed target. All three inputs are in integer yuan (initial >= 0, monthly > 0, target >= 0, assuming zero interest and zero withdrawals).

Use a while loop. Make sure that if initial already meets or exceeds target at the start, the function returns 0.

Initial Capital (yuan) Monthly Addition (yuan) Target Amount (yuan) Expected Months Scenario Description
1000 600 2200 2 Reaches exactly 2200 at end of Month 2
1000 600 2500 3 Reaches 2800 at end of Month 3 (passes)
2500 600 2500 0 Initial funds meet target; zero wait
3000 600 2500 0 Initial funds exceed target; return 0

Exercise 3: Purchasing Under Dual Budget and Inventory Constraints

A department buys identical office supplies one unit at a time. The unit price unit_price is a positive integer (in yuan), while supplier inventory stock (in units) and total budget budget (in yuan) are non-negative integers. Each unit purchased deducts unit_price from the remaining budget. Purchasing stops as soon as the remaining budget cannot cover another unit or supplier inventory runs out.

Write a function count_affordable_items(stock: int, unit_price: int, budget: int) -> int that returns the maximum number of units the department can buy. Combine a for loop over range(stock), a counter variable, and break: on each attempt, check whether the remaining budget is less than unit_price; if so, break immediately; otherwise deduct unit_price and increment the counter.

Supplier Stock (units) Unit Price (yuan) Total Budget (yuan) Expected Purchase Count Boundary Check Focus
10 80 250 3 Buying 3 leaves 10 yuan; cannot buy fourth unit
10 80 240 3 Buying 3 exhausts budget exactly (0 yuan left)
2 80 250 2 Budget allows more, but supplier stock is only 2 units
10 80 0 0 Zero budget allows zero purchases
0 80 250 0 Zero stock allows zero purchases

Exercise 4: Debugging State Updates in Fibonacci Recurrence

The function below tries to compute the n-th Fibonacci number (where Term 0 is 0 and Term 1 is 1) without using a temporary variable:

python
def incorrect_fibonacci(n: int) -> int:
    """Buggy implementation: overwrites previous state prematurely."""
    previous = 0
    current = 1

    for step in range(n):
        previous = current
        current = previous + current

    return previous


print(incorrect_fibonacci(3))

Running this code prints:

text
4

The first four Fibonacci terms (Terms 0, 1, 2, 3) are 0, 1, 1, 2, so Term 3 should be 2, not 4.

Before running the code, trace the two assignments inside the loop by hand and fill in the table below (recording the values of previous and current at the end of each iteration):

Completed Iteration Value of previous Value of current
0 (start) 0 1
1 To fill To fill
2 To fill To fill
3 To fill To fill

Once you see where the numbers go wrong, fix the function using a temporary variable and test it on all five cases below. Also explain: why does testing only n = 1 fail to catch this bug?

Input n Expected Return Focus
0 0 0 iterations; checks initial boundary
1 1 1 iteration (buggy code coincidentally returns 1 here too)
2 1 2 iterations; checks if Term 1 propagates correctly
3 2 Exposes the variable overwriting bug
6 8 Multiple iterations; checks recurrence stability

Reference Solutions and Implementation Guides

Exercise 1: Sum of Odd Integers from 1 to N

python
def sum_odd_numbers(n: int) -> int:
    """Return the sum of odd integers from 1 to n, where n is a non-negative integer."""
    total = 0
    for number in range(1, n + 1, 2):
        total += number
    return total


print(sum_odd_numbers(0))
print(sum_odd_numbers(1))
print(sum_odd_numbers(6))
print(sum_odd_numbers(7))

Output:

text
0
1
9
16

Implementation Guide: Using range(1, n + 1, 2) steps by 2 (1, 3, 5, ...), so the loop visits only odd integers without needing an if check inside the body. Setting the stop value to n + 1 guarantees that n is included whenever n itself is odd. Looping over range(1, n + 1) with if number % 2 != 0: works equally well.

Exercise 2: Minimum Months to Reach Investment Target

python
def months_to_target(initial: int, monthly: int, target: int) -> int:
    """Simulate monthly savings in integer yuan; initial and target are non-negative, monthly is positive."""
    reserve = initial
    months = 0

    while reserve < target:
        reserve += monthly
        months += 1

    return months


print(months_to_target(1000, 600, 2200))
print(months_to_target(1000, 600, 2500))
print(months_to_target(2500, 600, 2500))
print(months_to_target(3000, 600, 2500))

Output:

text
2
3
0
0

Implementation Guide: We start months = 0 before the loop. When initial is already at least target (as in the last two test cases), reserve < target is False on the very first check, so the loop body never runs and the function returns 0. Otherwise, each iteration adds monthly to reserve and increments months by 1 until reserve >= target.

Exercise 3: Purchasing Under Dual Budget and Inventory Constraints

python
def count_affordable_items(stock: int, unit_price: int, budget: int) -> int:
    """Return the number of items purchased; stock and budget are non-negative, unit_price is positive."""
    remaining = budget
    item_count = 0

    for attempt in range(stock):
        if remaining < unit_price:
            break
        remaining -= unit_price
        item_count += 1

    return item_count


print(count_affordable_items(10, 80, 250))
print(count_affordable_items(10, 80, 240))
print(count_affordable_items(2, 80, 250))
print(count_affordable_items(10, 80, 0))
print(count_affordable_items(0, 80, 250))

Output:

text
3
3
2
0
0

Implementation Guide: The two constraints are handled by two different parts of the loop: for attempt in range(stock): ensures we never buy more than the stock available, while if remaining < unit_price: break stops purchasing as soon as the remaining budget cannot pay for the next unit. When stock = 0, range(0) is empty and the function returns 0 immediately.

Exercise 4: Debugging State Updates in Fibonacci Recurrence

Tracing the buggy code gives:

Completed Iteration Value of previous Value of current Error Root Cause
0 (start) 0 1 Initial state correct
1 1 2 previous = current sets 1; then current = 1 + 1 sets 2
2 2 4 previous = current sets 2; then current = 2 + 2 sets 4
3 4 8 previous = current sets 4; then current = 4 + 4 sets 8

What went wrong? In iteration 1, the statement previous = current overwrites the original 0 in previous with 1. When the next line runs current = previous + current, instead of adding the old previous (0) to current (1), it adds the newly overwritten previous (1) to current (1), getting 1 + 1 = 2. Every iteration after that simply doubles current (2 \to 4 \to 8).

Why did testing n = 1 miss this bug? Because after 1 iteration, previous was set to current's initial value (1), which happens to match Term 1 (1) by coincidence; the corrupted value 2 was sitting in current, waiting to be copied into previous on iteration 2.

Corrected code:

python
def fibonacci(n: int) -> int:
    """Return the n-th Fibonacci number, where n >= 0 and Term 0 is 0."""
    previous = 0
    current = 1

    for step in range(n):
        next_value = previous + current
        previous = current
        current = next_value

    return previous


print(fibonacci(0))
print(fibonacci(1))
print(fibonacci(2))
print(fibonacci(3))
print(fibonacci(6))

Output:

text
0
1
1
2
8

Implementation Guide: Whenever you update two variables that depend on each other's old values, save the result of the calculation in a temporary variable (next_value) before overwriting either variable. And when testing a loop function, always test at least three cases: 0 iterations, 1 iteration, and multiple iterations (n \ge 2).


Core Concepts Part II

In Core Concepts Part I, every sequence we looped over was either generated by range() or computed from a formula. In real business operations, however, records rarely follow a neat arithmetic formula: customer order amounts (85.0, 99.0, 120.0 yuan) and daily store revenues (1200.0, 0.0, 1500.0 yuan) arrive as irregular numbers.

To process real datasets with loops, Core Concepts Part II introduces Python's primary sequence container—the list—and builds up four practical patterns step by step:

Learning Tier Sections Core Focus and Business Applications
1. List basics and traversal 5.7, 5.8 Store transactions in lists; iterate with for to filter, sum, and format records
2. Multi-scenario evaluation 5.9 Use nested loops to evaluate price \times quantity grids and project profits
3. Traversal helper tools 5.10, 5.11 Pair sequence numbers with values via enumerate(); handle search completions with loop else
4. Interactive continuous input 5.12 Use while True with sentinel values to record keyboard entries and aggregate results

Make sure you are comfortable with list operations and list traversal in Sections 5.7 and 5.8 before moving on to Sections 5.9 through 5.12.


5.7 Grouping Data in Containers: Three Basic List Operations

Suppose we have three customer orders of 85.0, 99.0, and 120.0 yuan. Because the gaps between these amounts are uneven (+14.0, then +21.0), range() cannot generate them. Nor do we want to create separate variables order_1, order_2, order_3, ..., order_1000 by hand, because separate variables cannot be stepped through automatically by a for loop.

Instead, we group these related numbers into a single ordered container called a list. This section covers the three list operations you will use most often with loops: creating a list, appending a new item to the end, and reading an item by its position.

5.7.1 Defining Lists: Storing a Collection of Items in Square Brackets

python
order_amounts = [85.0, 99.0, 120.0]

print(order_amounts)
print(f"Number of orders: {len(order_amounts)}")

Output:

text
[85.0, 99.0, 120.0]
Number of orders: 3

Writing comma-separated values inside square brackets [] creates a list. Each item stored inside the list is called an element, and the built-in function len(order_amounts) returns how many elements the list currently holds (3).

A list keeps its elements in the exact order you placed them and allows duplicate values. When you do not know the elements in advance, you can start with an empty list order_amounts = [] (whose length len(order_amounts) is 0) and add items as they arrive.

5.7.2 Appending Elements: .append()

When a checkout terminal records expenses one by one, each new expense is added to the end of an existing list using the .append() method:

python
expenses = []
expenses.append(25.0)
expenses.append(68.5)

print(expenses)
print(f"Recorded expenses: {len(expenses)}")

Output:

text
[25.0, 68.5]
Recorded expenses: 2

Here expenses starts empty ([]). Calling expenses.append(25.0) places 25.0 at the end of the list ([25.0]), and calling expenses.append(68.5) places 68.5 right after it ([25.0, 68.5]), bringing len(expenses) to 2.

Tip (Do Not Assign the Return Value of append): The .append() method modifies the list in place and returns None. Always call expenses.append(25.0) on its own line. Never write expenses = expenses.append(25.0)—doing so overwrites your list variable expenses with None!

5.7.3 Accessing Elements by Position: Zero-Based Indexing

To read a single element from a list, write the list name followed by the element's position number in square brackets []. This position number is called an index.

In Python, list indices start at 0, not 1.

Why start at 0? Because an index measures how many steps you move forward (the offset) from the first element. The first element sits right at the front of the list, so its offset is 0; the second element is 1 step from the front (index 1); and the third element is 2 steps from the front (index 2):

Everyday Description Python Index Access Expression Retrieved Amount
1st order 0 order_amounts[0] 85.0 yuan
2nd order 1 order_amounts[1] 99.0 yuan
3rd order 2 order_amounts[2] 120.0 yuan
python
order_amounts = [85.0, 99.0, 120.0]

print(f"First order amount: {order_amounts[0]:.2f} yuan")
print(f"Third order amount: {order_amounts[2]:.2f} yuan")

Output:

text
First order amount: 85.00 yuan
Third order amount: 120.00 yuan

In general, a list of length n has valid non-negative indices from 0 through n - 1. For our 3-element list order_amounts, the valid indices are 0, 1, and 2. Asking for order_amounts[3] raises an IndexError: list index out of range because there is no fourth element; likewise, indexing into an empty list (empty_list[0]) always raises an IndexError.

In-Class Exercise: Create a list containing two expense numbers, then call .append() to add a third. Print the list's length and its third element, confirming that the third element sits at index 2.


5.8 Processing List Elements with Loops

5.8.1 Accessing Elements Directly

To step through every element of a list with a for loop, simply place the list after in:

python
expenses = [25.0, 68.5, 16.5]

# For each expense in the expenses list:
#     Print expense formatted to 2 decimal places
for expense in expenses:
    print(f"Expense: {expense:.2f} yuan")

print("All expenses displayed.")

Output:

text
Expense: 25.00 yuan
Expense: 68.50 yuan
Expense: 16.50 yuan
All expenses displayed.

Notice the naming convention: the plural noun expenses names the entire list, while the singular noun expense names the loop variable that holds one item at a time. On Iteration 1, expense is 25.0; on Iteration 2, 68.5; and on Iteration 3, 16.5. In other words, for expense in expenses: hands expense each element's value directly, without needing range() or [i] indexing.

Stepping through every element of a collection in order is called traversal. Whenever you only need the values themselves, for item in items: is the cleanest way to traverse a list in Python. If the list is empty ([]), the loop body simply runs 0 times.

More generally, any for loop in Python follows the same pattern:

text
for loop_variable in iterable:
    process_current_element

An iterable is any object that can hand back its elements one at a time in a for ... in loop. Both range() objects and lists are iterables, as are strings and dictionaries (which we will meet in later chapters).

5.8.2 Sales Summary: Accumulation, Filtering, and Valid Records

Now let us combine list traversal, continue, an accumulator, and a counter to clean and summarize a store's daily sales records.

Suppose a retail store logs 4 days of sales in sales_records = [1200.0, -1.0, 0.0, 1500.0]. Normal daily sales are non-negative (\ge 0.0), whereas a missing daily report is marked with -1.0. We want to compute total sales and average daily sales across the valid reported days:

python
sales_records = [1200.0, -1.0, 0.0, 1500.0]
total_sales = 0.0
valid_count = 0  # Counter for valid reported days

# For each daily sales record in sales_records:
#     If sales is the missing marker -1.0, skip this iteration
#     Otherwise, add sales to total_sales and increment valid_count
for sales in sales_records:
    if sales == -1.0:
        continue
    total_sales += sales
    valid_count += 1

print(f"Reported days: {valid_count}")
print(f"Total reported sales: {total_sales:.2f} yuan")

if valid_count > 0:
    average_sales = total_sales / valid_count
    print(f"Average reported daily sales: {average_sales:.2f} yuan")
else:
    print("No valid records; cannot calculate average")

Output:

text
Reported days: 3
Total reported sales: 2700.00 yuan
Average reported daily sales: 900.00 yuan

Pay close attention to two data-cleaning rules in this calculation:

  1. Missing data (-1.0) is not the same as zero sales (0.0): On Day 2, -1.0 means the store failed to submit a report, so if sales == -1.0: continue skips that entry completely. On Day 3, 0.0 means the store was open and reported zero revenue; that is a genuine operating day and must increment valid_count by 1 (if you skipped 0.0, the average across the remaining two days would be overstated as 2700.0 / 2 = 1350.00 yuan instead of 900.00 yuan).
  2. Divide by valid_count, not len(sales_records): Although len(sales_records) is 4, only 3 days were actually reported. Dividing total_sales by valid_count (2700.0 / 3 = 900.00 yuan) gives the true daily average across reported days, and checking if valid_count > 0: first prevents division by zero if all entries are -1.0.

5.8.3 Order Calculation: Calling Single-Order Functions in a Loop

In Chapter 4, we wrote a function to compute the payable amount for one order. Once our orders are stored in a list, we can call that single-order function inside a for loop to bill an entire batch of orders.

Problem: An e-commerce store offers free shipping on orders of 99.0 yuan or more, and charges a 6.0-yuan shipping fee on smaller orders. Given a list of order amounts, compute the total payable amount across the entire batch.

python
def calculate_payment(order_amount: float) -> float:
    """Calculate payment including shipping for a single order amount."""
    if order_amount >= 99.0:
        shipping_fee = 0.0
    else:
        shipping_fee = 6.0
    return order_amount + shipping_fee


def calculate_batch_payment(order_amounts: list) -> float:
    """Calculate total payment across a list of orders; return 0.0 for empty batches."""
    total_payment = 0.0
    for amount in order_amounts:
        payment = calculate_payment(amount)
        total_payment += payment
    return total_payment


print(f"Batch total payment: {calculate_batch_payment([85.0, 99.0, 120.0]):.2f} yuan")
print(f"Empty batch total payment: {calculate_batch_payment([]):.2f} yuan")

Output:

text
Batch total payment: 310.00 yuan
Empty batch total payment: 0.00 yuan

For the three orders [85.0, 99.0, 120.0], calculate_payment() returns 91.0 (85.0 + 6.0), 99.0, and 120.0 yuan, which calculate_batch_payment() sums to 310.00 yuan.

Splitting the task across these two functions illustrates separation of concerns:

  • calculate_payment() focuses on the business rule for a single order (how shipping is charged).
  • calculate_batch_payment() focuses on looping over the list and summing the results.

If the store later changes its free-shipping threshold from 99.0 yuan to 129.0 yuan, you only need to update calculate_payment(); the batch loop in calculate_batch_payment() stays untouched.

In-Class Exercise: Append a fourth order of 98.0 yuan to the list [85.0, 99.0, 120.0], work out the new batch total by hand, and run the code to verify. Also trace step by step why calculate_batch_payment([]) returns 0.0.


5.9 Repeating for Each Item: Nested Loops

5.9.1 Evaluating Combinations of Prices and Quantities

In managerial decision-making, we often want to see how two variables interact across a grid of scenarios. Suppose we are pricing a product with a unit variable cost of 30.0 yuan and fixed costs of 1000.0 yuan:

\text{Profit} = (\text{Price} - \text{Unit Cost}) \times \text{Quantity} - \text{Fixed Cost}

We want to evaluate profit across two candidate unit prices (50.0 and 60.0 yuan) and two projected sales quantities (100 and 200 units).

How do we test all 2 \times 2 = 4 combinations systematically? First we hold the price at 50.0 yuan and loop through both quantities (100 and 200); then we advance the price to 60.0 yuan and loop through both quantities (100 and 200) again. Placing one loop inside another like this is called a nested loop:

python
prices = [50.0, 60.0]
quantities = [100, 200]
unit_cost = 30.0
fixed_cost = 1000.0

# For each unit price in candidate prices:
#     For each sales quantity in candidate quantities:
#         Calculate and print profit for the (price, quantity) combination
for price in prices:
    for quantity in quantities:
        profit = (price - unit_cost) * quantity - fixed_cost
        print(f"Price: {price:.2f} yuan, Quantity: {quantity} units, Profit: {profit:.2f} yuan")

Output:

text
Price: 50.00 yuan, Quantity: 100 units, Profit: 1000.00 yuan
Price: 50.00 yuan, Quantity: 200 units, Profit: 3000.00 yuan
Price: 60.00 yuan, Quantity: 100 units, Profit: 2000.00 yuan
Price: 60.00 yuan, Quantity: 200 units, Profit: 5000.00 yuan

A nested loop works like the hour and minute hands of a clock: each time the outer loop advances by one step, the inner loop runs through a complete cycle from start to finish:

  1. The outer loop sets price = 50.0.
  2. While price stays at 50.0, the inner loop runs for quantity = 100 (profit 1000.00) and then quantity = 200 (profit 3000.00), completing its first full pass.
  3. The outer loop advances to price = 60.0.
  4. The inner loop starts fresh from the beginning of quantities, running for quantity = 100 (profit 2000.00) and quantity = 200 (profit 5000.00).
Step Outer price Inner quantity Resulting profit
1 50.0 yuan 100 units 1000.00 yuan
2 50.0 yuan 200 units 3000.00 yuan
3 60.0 yuan 100 units 2000.00 yuan
4 60.0 yuan 200 units 5000.00 yuan

In mathematics, pairing every item of prices with every item of quantities forms their Cartesian product. If prices has m elements and quantities has n elements, the inner loop body runs m \times n times in total (here 2 \times 2 = 4 times; for 10 prices and 12 quantities, the exact same four lines of code evaluate all 10 \times 12 = 120 scenarios).

5.9.2 break Only Exits the Innermost Loop

Now suppose our question changes slightly: for each candidate price in [50.0, 60.0], we want to find only the first sales quantity in [100, 200, 300] that achieves a profit of at least 2500.0 yuan, and then move right on to the next price.

To stop checking larger quantities once a price hits target_profit, we place break inside the inner loop:

python
prices = [50.0, 60.0]
quantities = [100, 200, 300]  # Candidate quantities in ascending order
unit_cost = 30.0
fixed_cost = 1000.0
target_profit = 2500.0

for price in prices:
    for quantity in quantities:
        profit = (price - unit_cost) * quantity - fixed_cost
        if profit >= target_profit:
            print(f"Price {price:.2f} yuan: first qualifying volume is {quantity} units")
            break

Output:

text
Price 50.00 yuan: first qualifying volume is 200 units
Price 60.00 yuan: first qualifying volume is 200 units

Look at the second line of output: even though break ran when price was 50.0 yuan, the program still moved on to test price = 60.0 yuan! That illustrates the scoping rule for break in nested loops: break exits only the innermost loop that directly contains it; the outer loop continues to its next iteration as usual.

When price = 50.0, the inner loop tests 100 units (profit 1000.0, below 2500.0) and then 200 units (profit 3000.0, which meets 2500.0). Running break terminates the inner quantity loop—skipping 300 units for 50.0 yuan—and hands control back to the outer loop, which advances price to 60.0 yuan and starts the inner quantity loop over from 100 units.

In-Class Exercise: Change target_profit to 3500.0 yuan. Work out on paper which quantity qualifies first for 50.0 yuan and for 60.0 yuan, then run the script to check.


5.10 Retrieving Both Index and Value: enumerate()

In Section 5.8.1, for amount in order_amounts: gave us each order amount directly, without its position number. When printing an invoice or audit ledger, however, we often want to display both a 1-based sequence number (Order #1, Order #2, ...) and the order amount on the same line.

Instead of initializing a separate counter order_no = 1 before the loop and incrementing order_no += 1 inside the body, we can wrap the list in Python's built-in enumerate() function, which hands back a (number, element) pair on each iteration:

python
order_amounts = [85.0, 99.0, 120.0]

for order_no, amount in enumerate(order_amounts, start=1):
    print(f"Order #{order_no}: {amount:.2f} yuan")

Output:

text
Order #1: 85.00 yuan
Order #2: 99.00 yuan
Order #3: 120.00 yuan

On each iteration, enumerate(order_amounts, start=1) produces a pair (current_number, current_element). Writing two variable names order_no, amount separated by a comma between for and in splits that pair into order_no and amount automatically—a syntax known as unpacking:

Iteration Paired Data from enumerate() Assigned to order_no Assigned to amount
Round 1 (1, 85.0) 1 85.0 yuan
Round 2 (2, 99.0) 2 99.0 yuan
Round 3 (3, 120.0) 3 120.0 yuan

Here start=1 tells enumerate() to begin numbering at 1 instead of the default 0. Keep in mind that start=1 only changes the label number assigned to order_no; it still starts at the very first element of the list (85.0) and never skips any elements.

In practice, follow this simple rule of thumb: if you only need the elements, write for item in items:; if you need both a sequence number and the element, write for i, item in enumerate(items, start=1):.


5.11 When a Loop is Not Interrupted by break: The else Clause

5.11.1 Using Loop else to Express "Search Completed Without Finding a Match"

Recall Section 5.5.1, where we searched for a qualifying sales quantity and used a boolean flag variable found = False so we could print a notice after the loop if no match was found. Because "search a sequence and do something if no break occurred" is such a common pattern, Python allows you to attach an else clause directly to a for or while loop (aligned vertically with for or while).

How does a loop else clause work? The else block runs if and only if the loop finishes normally without hitting break; if the loop exits early via break, the else block is skipped.

Suppose a bank credit audit inspects a customer's loan history in overdue_days. If any loan has days > 0, the script prints an overdue alert and exits the loop immediately with break; if all loans have 0 overdue days, the script reports "No overdue records found":

python
overdue_days = [0, 0, 0]

for days in overdue_days:
    if days > 0:
        print(f"Overdue record found: {days} days overdue")
        break
else:
    print("No overdue records found")

print("Audit finished")

Output:

text
No overdue records found
Audit finished

Look closely at the indentation: else: sits at the exact same column as for, not indented under if. Because every item in [0, 0, 0] is 0, if days > 0: is never True, so break never runs. Once the for loop finishes checking all three items normally, Python enters the else: block and prints "No overdue records found".

Compare what happens across three different input lists:

Input overdue_days How the Loop Ends Execute Loop else?
[0, 0, 0] (all clean) Checks all 3 items; no break hit Yes
[0, 12, 0] (overdue exists) Hits break on second item (12) No
[] (empty list) 0 iterations; no break hit Yes

5.11.2 Do Not Confuse Loop else with "Loop Did Not Run"

Many beginners look at for ... else and guess that else runs when the loop body does not run. Row 1 of the table above shows why that guess is wrong: for [0, 0, 0], the loop body runs 3 times, and the else block still runs afterward!

The easiest way to remember loop else is to read else: mentally as no_break:

  • Did the loop hit break? \rightarrow Yes: exit the loop and skip else:.
  • Did the loop finish without hitting break? (whether it checked all items, used continue, or ran 0 times on an empty list) \rightarrow No break occurred: run else:.

Tip (Code Style Preference): Because loop else does not exist in C, Java, or JavaScript, some engineering teams prefer writing an explicit flag variable (found = False) or placing the search inside a function that exits early with return (as you will do in Part II, Exercise 2). Any of the three styles is fine when writing your own code, but you should be able to read for ... else whenever you encounter it in Python codebases.


5.12 Continuous Input: Sentinel Values and Interactive Loops

In Section 5.8.2, the store's daily sales were already sitting in a predefined list sales_records = [1200.0, -1.0, 0.0, 1500.0]. At a real point-of-sale terminal, however, a cashier types in sales figures one by one at the keyboard, and the program does not know in advance how many entries will be typed.

To handle interactive data entry cleanly, we split the program into two stages:

  1. Stage 1 (Interactive Entry): Run a while loop that prompts the operator for numbers one by one and appends each valid number to a list sales_records.
  2. Stage 2 (Summary Aggregation): Once entry is finished and the while loop ends, run a for loop over sales_records to compute the total and count.

Why not just add the numbers on the fly inside Stage 1 without a list? Because storing the validated entries in sales_records preserves the individual daily records in memory so Stage 2 (or later audit steps) can inspect, sum, or average them cleanly.

In Stage 1, how does the operator tell the program "I am done entering data"? We pick a special number that could never occur as a real daily sales figure to act as the stop signal. A special value used only to signal the end of input is called a sentinel value.

Because real daily sales are always non-negative (\ge 0.0), we choose -1 as our sentinel value: typing -1 stops the input loop; typing any other negative number prints an error prompt and asks again; typing any non-negative number appends it to sales_records.

Tip (Input Type Assumptions): The script below uses float(input(...)) to convert the typed string into a float, assuming the operator types valid digits. We will learn how to catch non-numeric text (such as "abc") gracefully with try ... except in Chapter 8.

python
sales_records = []

# Continuously receive keyboard input:
#     If sentinel -1.0 is entered, break immediately
#     If any other negative value is entered, prompt re-entry and skip
#     If a valid non-negative number is entered, append to sales_records
while True:
    amount = float(input("Enter daily sales (-1 to stop): "))

    if amount == -1.0:
        break
    if amount < 0.0:
        print("Amount cannot be negative; please re-enter")
        continue

    sales_records.append(amount)

total_sales = 0.0
for amount in sales_records:
    total_sales += amount
record_count = len(sales_records)

print(f"Reported days: {record_count}")
print(f"Total reported sales: {total_sales:.2f} yuan")

If the operator types 120, -2, 0, 80, and -1 in order, the terminal session looks like this:

text
Enter daily sales (-1 to stop): 120
Enter daily sales (-1 to stop): -2
Amount cannot be negative; please re-enter
Enter daily sales (-1 to stop): 0
Enter daily sales (-1 to stop): 80
Enter daily sales (-1 to stop): -1
Reported days: 3
Total reported sales: 200.00 yuan

Notice how four details work together in Stage 1:

  1. while True: paired with break: Because we need to read amount = float(input(...)) before we can test whether amount == -1.0, writing while True: starts an open-ended loop and lets if amount == -1.0: break stop the loop right after the input is read.
  2. Test the sentinel -1.0 before testing < 0.0: Because -1.0 is itself a negative number, if amount == -1.0: must come first. If you put if amount < 0.0: first, typing -1 would be caught as an invalid negative number, print the error message, and hit continue—making it impossible to exit the loop!
  3. Neither -1.0 nor invalid negatives enter sales_records: Both break (for -1.0) and continue (for other negative numbers like -2) jump out before reaching sales_records.append(amount). Only valid sales figures (120.0, 0.0, 80.0) are appended to the list.
  4. Why len(sales_records) works directly here: Compare this with Section 5.8.2, where -1.0 was stored inside sales_records as a missing-data marker and we had to maintain a separate counter valid_count. Here, -1.0 is intercepted at the keyboard and never enters sales_records, so len(sales_records) (3) is already the exact count of valid reported days.

Core Concepts Part II Exercises

Exercise 1: All Qualifying Price-Quantity Combinations

Using the cost parameters from Section 5.9 (unit_cost = 30.0 yuan, fixed_cost = 1000.0 yuan), evaluate candidate prices prices = [50.0, 60.0] and candidate quantities quantities = [100, 200, 300] against a target profit of 3000.0 yuan.

Write a two-level nested loop (prices on the outer loop, quantities on the inner loop) that prints every (price, quantity) combination earning a profit of at least 3000.0 yuan, and then prints the total count of qualifying plans at the end.

Hint: Because management wants all qualifying combinations listed (all 4 of them), do not use break when a match is found. Also check where you initialize your plan counter so it does not reset between prices.

Exercise 2: Locate First Over-Threshold Order

During an internal audit, you need to scan a list of order amounts in sequence and find the first order that strictly exceeds (>) an audit threshold. Write a function find_first_large_order(order_amounts: list, threshold: float) -> int that takes a list of non-negative order amounts order_amounts and a non-negative threshold, and returns the 1-based order number of the first order with amount > threshold. If no order strictly exceeds threshold (or if order_amounts is empty), return 0.

Use enumerate(order_amounts, start=1). Think about why you can place return order_no directly inside the if branch in the loop without needing break or a flag variable.

Order List order_amounts Threshold threshold Expected Order Number Scenario Description
[500.0, 1000.0, 1200.0, 1500.0] 1000.0 3 Order 2 equals threshold (not over); Order 3 is first over
[1200.0, 500.0] 1000.0 1 Order 1 exceeds threshold immediately; returns 1
[500.0, 1000.0] 1000.0 0 No order strictly exceeds threshold; returns 0
[] 1000.0 0 Empty list; loop does not run; returns 0

Exercise 3: Interactive Daily Sales Entry and Safe Average

Extend the two-stage interactive sales script from Section 5.12 so that Stage 2 also computes and prints the average daily sales across reported days (assuming valid numeric strings entered in yuan).

Keep the Stage 1 input rules identical: -1 ends input; any other negative number prints "Amount cannot be negative; please re-enter" and skips to the next prompt; non-negative numbers (>= 0.0) are appended to sales_records. At the end of Stage 2:

  • If at least one valid day was recorded (for example, 120, -2, 0, 80, -1), compute and print the average daily sales formatted to 2 decimal places.
  • If the operator types -1 right away without recording any valid days (0 reported days), do not divide by zero; instead print "No valid records; cannot calculate average".
Input Sequence via Keyboard Reported Days Total Sales Expected Output or Notice
120, -2, 0, 80, -1 3 days 200.00 yuan 66.67 yuan
0, -1 1 day 0.00 yuan 0.00 yuan
-1 0 days 0.00 yuan No valid records; cannot calculate average

Reference Solutions and Implementation Guides

Exercise 1: All Qualifying Price-Quantity Combinations

python
prices = [50.0, 60.0]
quantities = [100, 200, 300]
unit_cost = 30.0
fixed_cost = 1000.0
target_profit = 3000.0
plan_count = 0

for price in prices:
    for quantity in quantities:
        profit = (price - unit_cost) * quantity - fixed_cost
        if profit >= target_profit:
            plan_count += 1
            print(f"Price: {price:.2f} yuan, Quantity: {quantity} units, Profit: {profit:.2f} yuan")

print(f"Qualifying plans: {plan_count}")

Output:

text
Price: 50.00 yuan, Quantity: 200 units, Profit: 3000.00 yuan
Price: 50.00 yuan, Quantity: 300 units, Profit: 5000.00 yuan
Price: 60.00 yuan, Quantity: 200 units, Profit: 5000.00 yuan
Price: 60.00 yuan, Quantity: 300 units, Profit: 8000.00 yuan
Qualifying plans: 4

Implementation Guide: Make sure plan_count = 0 is initialized before the outer for price in prices: loop. If you accidentally place plan_count = 0 between the outer header and the inner header, plan_count resets to 0 on every new price and ends up counting only the qualifying plans for the last price (60.0 yuan, reporting 2 instead of 4).

Exercise 2: Locate First Over-Threshold Order

python
def find_first_large_order(order_amounts: list, threshold: float) -> int:
    """Return 1-based order number of the first order exceeding threshold; return 0 if none found."""
    for order_no, amount in enumerate(order_amounts, start=1):
        if amount > threshold:
            return order_no
    return 0


print(find_first_large_order([500.0, 1000.0, 1200.0, 1500.0], 1000.0))
print(find_first_large_order([1200.0, 500.0], 1000.0))
print(find_first_large_order([500.0, 1000.0], 1000.0))
print(find_first_large_order([], 1000.0))

Output:

text
3
1
0
0

Implementation Guide: Because we inspect orders in ascending order (start=1), the first time amount > threshold holds is guaranteed to be the first over-threshold order. Executing return order_no inside the if branch immediately exits both the loop and the function, handing back order_no without needing break or a flag variable. If the loop finishes all items without ever triggering return order_no (or if order_amounts is empty), execution reaches the final line return 0.

Exercise 3: Interactive Daily Sales Entry and Safe Average

python
sales_records = []

while True:
    amount = float(input("Enter daily sales (-1 to stop): "))

    if amount == -1.0:
        break
    if amount < 0.0:
        print("Amount cannot be negative; please re-enter")
        continue

    sales_records.append(amount)

total_sales = 0.0
for amount in sales_records:
    total_sales += amount
record_count = len(sales_records)

print(f"Reported days: {record_count}")
print(f"Total reported sales: {total_sales:.2f} yuan")

if record_count > 0:
    average_sales = total_sales / record_count
    print(f"Average reported daily sales: {average_sales:.2f} yuan")
else:
    print("No valid records; cannot calculate average")

For inputs 120, -2, 0, 80, and -1:

text
Reported days: 3
Total reported sales: 200.00 yuan
Average reported daily sales: 66.67 yuan

Implementation Guide: Because Stage 1 filters out both the sentinel -1.0 and invalid negative numbers before .append(), sales_records contains only valid reported days. In Stage 2, record_count = len(sales_records) gives the exact number of valid days, and if record_count > 0: protects the division total_sales / record_count when the operator exits immediately on the first prompt.


Chapter Summary

Across every business problem in this chapter, writing a loop follows the same three-step rhythm: initialize state variables before the loop, update those variables on each iteration inside the loop, and print or return the final result after the loop ends.

The table below summarizes all core loop and list patterns from this chapter:

Business Need or Scenario Recommended Syntax Key Principle and Error Prevention
Create empty list and append records items = [] then items.append(x) .append() mutates in place; never write items = items.append(x)
Access element by offset index items[0], items[1] Index is offset distance; zero-based; guard against index out of range
Traverse every item in a list for item in items: Variable receives elements directly; no manual indexing required
Iterate over fixed integer range for i in range(start, stop, step): Half-open [start, stop); to include integer n, write stop as n + 1
Continue until condition is met while condition: Loop body must update state variables toward termination condition
Accumulate sums or count occurrences Initialize to 0 before; update inside Never initialize inside the loop body, or state resets to 0 each cycle
Process only matching records Nest if statements inside loop body Pay attention to indentation: only indented statements are conditionally run
Skip special records and continue Use continue inside loop body Skips remainder of current iteration; in while, do not skip step increments
Terminate loop on early match Use break inside loop body Exits innermost enclosing loop immediately; outer loops continue running
Return early from inside function Use return inside function loop Exits entire function immediately; place after loop for aggregate summaries
Retrieve both index and value for no, item in enumerate(items, start=1): Unpack paired tuples; avoids maintaining manual counter variables
Evaluate price \times quantity grid Two-level nested loops Inner loop completes full cycle for each outer step; total runs equal product

Whenever you test a loop you have written, check these five boundary cases:

  1. Empty inputs (an empty list [], month_count = 0, or typing the sentinel -1 right away): Does the loop skip its body cleanly, and does your code guard against dividing by zero or indexing into an empty list?
  2. Single-item inputs (n = 1 or a 1-element list): Does the loop run once and return the right value?
  3. Exact boundary values (an order amount of exactly 99.0 yuan, or a profit that equals target_profit exactly): Did you choose < vs. <= (and > vs. >=) accurately, and does range() include the upper bound you intended?
  4. Zero matches (a target profit no candidate quantity can reach, or a list where no item passes the filter): Does your flag variable, loop else clause, or fallback return handle the "not found" case properly?
  5. Already-satisfied starting state (initial reserve already meets or exceeds target in a while loop): Does the while condition evaluate to False on Check 1 and return 0 months without running the body?

Supplementary Notes (Tips)

Tip 1: Do Not Modify Loop Variables Inside a for Loop Body

In a Python for loop, the interpreter automatically fetches the next element from the iterable at the top of each iteration and assigns it to the loop variable. Even if you reassign the loop variable inside the loop body (for example, writing number = 999), the very next iteration will overwrite number with the next item from the sequence anyway. Never modify a for loop variable inside the loop body to try to jump ahead or change the step size. If you need a different step size, pass step to range(start, stop, step); if you need to skip certain items, use continue.

Also remember that in Python, for, while, and if blocks do not create their own separate variable scope. If a loop runs at least one iteration, the loop variable stays defined after the loop ends, holding whatever value it had on the last iteration; if the loop runs 0 iterations (for example, over an empty list []), the loop variable is never created at all. For that reason, always initialize variables that you plan to read after the loop (total, count, found) before the loop starts rather than relying on the loop variable after the loop.

Finally, keep all list traversals (for item in items:) read-only with respect to the list's structure: do not insert or delete elements from items while iterating over it directly, because shifting elements in memory mid-traversal causes the loop to skip adjacent items. We will cover safe ways to build filtered lists in the list chapter.

Tip 2: Floating-Point Errors and Financial Boundary Checks

To keep our early examples simple, several scripts in this chapter stored currency amounts as standard floating-point numbers (float). Under the hood, computers store float values in base-2 (binary) scientific notation following the IEEE 754 standard. Just as \frac{1}{3} = 0.3333\dots cannot be written as a finite decimal in base 10, the decimal fractions 0.1 and 0.2 are infinite repeating fractions in base 2. Because the computer has to round them to a fixed number of binary digits, 0.1 + 0.2 evaluates to 0.30000000000000004 rather than 0.3, and adding float values repeatedly inside a loop can accumulate tiny rounding discrepancies.

When working with monetary amounts or decimals in loops, follow two practical rules:

  1. Never use exact floating-point equality (== or !=) as a while loop stopping condition. If x starts at 0.0 and adds 0.1 on each step, while x != 1.0: will run forever because after 10 additions x is 0.9999999999999999, which is not equal to 1.0, and then steps right past 1.0 to 1.0999999999999999. Always use inequality bounds such as while x < 1.0: (or integer counters).
  2. In real accounting and financial software, do not store money as raw float values. Instead, store amounts as integer cents (int, where 1200 yuan is 120000 fen/cents) or use Python's standard-library decimal.Decimal type, which performs exact base-10 arithmetic.

Keep in mind too that formatting a number with print(f"{average_cost:.2f}") only rounds the displayed text to 2 decimal places; it does not change the underlying number stored in average_cost.

Tip 3: When to Write a Loop vs. When to Call sum()

If you already have a clean list of numbers such as daily_sales = [1200.0, 1500.0, 1300.0] and simply want their total, Python's built-in sum() function computes the answer in a single line:

python
daily_sales = [1200.0, 1500.0, 1300.0]
total_sales = sum(daily_sales)
print(f"Total sales: {total_sales:.2f} yuan")

Output:

text
Total sales: 4000.00 yuan

If sum(daily_sales) is so short, why did we spend this chapter learning how to write accumulator loops by hand?

Because sum() only knows how to add every element of an existing collection unconditionally. As soon as your business rule requires something more—skipping -1.0 missing-data markers while counting valid days (Section 5.8.2), computing per-order shipping fees before adding each order to the batch total (Section 5.8.3), or stopping early with break the moment a purchase would exceed the remaining budget (Part I, Exercise 3)—sum() alone cannot express the rule. Writing an explicit loop gives you full control over what happens on each step. In everyday Python coding, use sum() whenever you are adding a clean sequence as-is, and write a loop whenever you need to filter, count, transform, or stop early.


Navigation: Previous: Chapter 4 - if Statements and Branching Structures