Computer Programming 1 (Python) • Control Flow

Avoiding Loop Bugs — I can avoid common loop bugs (off-by-one, wrong bounds) by testing boundary cases.

Master the art of writing correct loops by understanding off-by-one errors and testing edge cases.

A Python coding terminal with a program flow and laptop.
Inside this lesson 10 sections 5 questions 8 figures

SECTION 1

Historical Context & Motivation

Loops are one of the most powerful constructs in programming, yet they have been a persistent source of bugs since the earliest days of computing. When programmers write a loop that iterates one time too many or one time too few, the result is called an off-by-one error (sometimes abbreviated OBOE). These bugs are so common that they have earned a legendary status among software developers, and entire categories of software failures — from security vulnerabilities to crashed spacecraft — trace back to simple loop boundary mistakes.

1960s • Fencepost Problem Named

Computer scientists begin referring to off-by-one errors as the fencepost problem: if you build a 100-foot fence with posts every 10 feet, you need 11 posts, not 10. This analogy sticks for decades.

1988 • Morris Worm Exploits Boundary Bug

The Morris Worm, one of the first internet worms, exploited a buffer overflow — a close cousin of off-by-one errors — in Unix systems, infecting roughly 6,000 computers and demonstrating the real-world danger of boundary mistakes.

1991 • Python Introduced with Zero-Based Indexing

Guido van Rossum releases Python, choosing zero-based indexing and the range() function with an exclusive upper bound, a design decision intended to reduce off-by-one errors.

2000s • Unit Testing Becomes Standard

The rise of test-driven development (TDD) encourages programmers to write boundary test cases before coding their loops, catching off-by-one errors early in the development process.

Today • Boundary Testing in Modern Practice

Modern IDEs, linters, and automated testing frameworks flag potential loop boundary issues, but understanding why these errors happen remains an essential skill for every programmer.

The central question this lesson addresses is deceptively simple: how do you write a loop that runs exactly the right number of times, and how do you prove to yourself that it does? By the end of this lesson, you will have a systematic approach to writing bug-free loops and testing them at their boundaries.

SECTION 2

Core Principles & Definitions

Before diving into specific bug patterns, you need to understand the foundational ideas behind correct loop construction. Every loop involves a starting point, a stopping condition, and a step size, and getting any one of these wrong can produce a bug. The following principles form the basis for writing and verifying loops.

1

Off-by-One Error (OBOE)

A bug where a loop iterates one time too many or one time too few. This typically happens when you confuse < with <= or start at 1 instead of 0.
2

Wrong Bounds

Choosing an incorrect start or stop value for the loop. For example, using range(1, n) when you meant range(0, n) skips the first element entirely.
3

Boundary Cases

The extreme or edge inputs for a loop: the smallest valid input, the largest, an empty collection, or a collection with exactly one element. Testing these cases exposes most loop bugs.
4

Inclusive vs. Exclusive Bounds

Python's range(a, b) includes a (inclusive) but excludes b (exclusive). Forgetting this convention is the #1 source of Python loop bugs.
5

Loop Invariant Thinking

A mental technique where you state what must be true before, during, and after the loop. If you can describe what your loop guarantees after every iteration, you can verify its correctness.
✦

KEY TAKEAWAY

Think of a loop like setting an alarm clock. If you want to wake up at 7:00 AM and you set your alarm for 6:59 or 7:01, you are off by one. The alarm still goes off, and you might not even notice the mistake — until the day it matters. Testing boundary cases is like double-checking your alarm the night before an important exam.

SECTION 3

Visual Explanation — How Off-by-One Errors Happen

The diagram below illustrates how Python's range() function works and where off-by-one mistakes typically occur. Pay close attention to which indices are included and which are excluded.

Python range() — Inclusive Start, Exclusive Stoprange(0, 5) → produces: 0, 1, 2, 3, 4Index Positions:012345EXCLUDED✓ included✓ CORRECTfor i in range(0, 5): # iterates 5 times: i = 0,1,2,3,4✗ BUG: One too fewfor i in range(0, 4): # iterates 4 times: misses index 4✗ BUG: One too manyfor i in range(0, 6): # iterates 6 times: includes index 5
The green boxes show the indices produced by range(0, 5). The red dashed box shows index 5, which is excluded because Python's range uses an exclusive upper bound. The two red rows show common off-by-one mistakes.

Notice the pattern: Python's range(a, b) always produces exactly b − a values. So range(0, 5) gives you 5 − 0 = 5 values. If you need to visit every index of a list with 5 elements, range(0, 5) or equivalently range(len(my_list)) is exactly right. Adjusting either bound by 1 introduces a bug that might go unnoticed until the program encounters an edge case.

SECTION 4

How Loop Bugs Work — The Mechanics

Understanding the exact mechanics of common loop bugs helps you prevent them. Let's look at the two most frequent categories: off-by-one with range() and wrong comparison operators in while loops.

The range() Iteration Count Formula

ITERATION COUNT

iterations=(stop−start)÷stepiterations = (stop − start) ÷ step

Where start is the first value (default 0), stop is the exclusive upper bound, and step is the increment (default 1). The result is rounded down to the nearest integer.

For example, range(2, 10, 3) produces (10 − 2) ÷ 3 = 2.67, rounded down to 3 iterations (values 2, 5, 8). Knowing this formula lets you verify your loop's iteration count before running it.

While Loop Comparison Traps

WHILE LOOP PATTERN

whilecounter<limit:→runslimit−countertimeswhilecounter<=limit:→runslimit−counter+1timeswhile counter < limit: → runs limit − counter times while counter <= limit: → runs limit − counter + 1 times

Switching between < and <= changes the iteration count by exactly 1. This single-character difference is the most common while-loop bug.

⚠

Common Trap

When iterating over a list with a while loop, using while i <= len(my_list) causes an IndexError because the last valid index is len(my_list) - 1. Always use while i < len(my_list) instead.

The Fencepost Formula

FENCEPOST PRINCIPLE

posts=sections+1posts = sections + 1

A fence with n sections requires n + 1 posts. In programming: if you need to print commas between n items, you print n − 1 commas.

SECTION 5

Boundary Testing — Your Bug-Catching Safety Net

The most effective way to catch loop bugs is to test your code with boundary cases — inputs that push your loop to its extremes. If your loop works correctly for boundary cases, it almost certainly works for everything in between. The table below shows the essential boundary cases you should test for any loop.

Boundary CaseWhy It Catches BugsExample Input
Empty inputLoop should run 0 times; catches accidental first-iteration executionmy_list = []
Single elementCatches fencepost errors (e.g., printing commas between 1 item)my_list = [42]
Two elementsSmallest input that tests the transition between iterationsmy_list = [1, 2]
First elementChecks that index 0 is not skipped (common with range(1, n))Verify output includes the item at index 0
Last elementChecks that the final item is processed (catches range(0, n−1) bugs)Verify output includes the item at index n−1
Typical/middle caseConfirms the loop works under normal conditionsmy_list = [3, 7, 1, 9, 4]

Essential boundary cases for testing loops

Boundary Testing Decision FlowchartWrite your loopTest with empty input (n = 0)✓ Pass → Next test✗ Fail → Fix boundsTest with single element (n = 1)Test first & last element access✓ All pass → Loop is correctAdjust start/stop/operatorthen
Follow this flowchart every time you write a loop. Start by testing with an empty input, then a single element, then verify the first and last elements are handled correctly.

The key insight is that you don't need to test every possible input. If your loop handles the empty case, the single-element case, and the first and last elements correctly, then the intermediate cases are almost always correct too. This is because off-by-one errors are consistent — if the loop misses the last element for a 5-element list, it will also miss it for a 100-element list.

SECTION 6

Worked Example — Finding and Fixing a Loop Bug

Let's walk through a realistic scenario where a student writes a function to find the maximum value in a list, introduces an off-by-one bug, and uses boundary testing to find and fix it.

Debugging a Maximum-Value Function

  1. 1

    Step 1 — Write the Initial (Buggy) Code

    A student writes the following function to find the largest number in a list: def find_max(numbers): biggest = numbers[0] for i in range(1, len(numbers) - 1): if numbers[i] > biggest: biggest = numbers[i] return biggest At first glance, the code looks reasonable. The loop starts at index 1 (since index 0 is already stored in biggest), and it seems to iterate through the rest.
  2. 2

    Step 2 — Test with a Typical Case

    Testing with find_max([3, 7, 1, 9, 4]): the loop checks indices 1, 2, and 3 (because range(1, 4) stops before index 4). It finds 9 at index 3, so it returns 9. This looks correct — but only by luck, because the maximum isn't at the last index.
    Output: 9 ✓ (appears correct, but the last element was never checked!)
  3. 3

    Step 3 — Test Boundary Case: Maximum at Last Index

    Now test with find_max([3, 1, 7, 2, 10]). The maximum is 10, at index 4. The loop runs range(1, 4), so it checks indices 1, 2, 3 but never checks index 4. It returns 7 — wrong!
    Output: 7 ✗ (Bug found! The last element is skipped.)
  4. 4

    Step 4 — Identify the Bug

    The bug is in range(1, len(numbers) - 1). The student subtracted 1 from len(numbers) thinking Python's exclusive upper bound already accounts for it. But range() is already exclusive, so subtracting 1 again skips the last element. This is a classic off-by-one error.
  5. 5

    Step 5 — Fix and Re-Test

    The fix is simple — change the upper bound to len(numbers): for i in range(1, len(numbers)): Now re-test all boundary cases: empty list (add a guard clause), single element, max at first position, max at last position, and a typical middle case.
    find_max([3, 1, 7, 2, 10]) → 10 ✓
  6. 6

    Step 6 — Add Empty-List Protection

    Testing find_max([]) causes an IndexError on numbers[0]. Add a guard: def find_max(numbers): if len(numbers) == 0: return None biggest = numbers[0] for i in range(1, len(numbers)): if numbers[i] > biggest: biggest = numbers[i] return biggest
    All boundary cases pass. The function is correct.

SECTION 7

Common Bug Patterns & How to Avoid Them

Most loop bugs fall into a small number of recurring patterns. The table below summarizes the most common ones, showing the buggy code, what goes wrong, and the corrected version.

Bug PatternBuggy CodeFixed Code
Double subtraction — subtracting 1 when range() already excludes the stoprange(0, len(lst) - 1)range(0, len(lst))
Starting at 1 — skipping the first elementrange(1, len(lst))range(0, len(lst))
Wrong while operator — <= instead of <while i <= len(lst):while i < len(lst):
Fencepost in joins — extra separator at endfor item in lst: result += item + ","",".join(lst)
Empty list crash — no guard clausebiggest = lst[0]if not lst: return None biggest = lst[0]

Five common loop bug patterns with fixes

🔑

KEY TAKEAWAY

Think of these bug patterns like the most common typos in English — "their" vs. "there" vs. "they're." Once you memorize the short list of mistakes people make, you can proofread your own code much more quickly. Keeping a personal "bug diary" of mistakes you've made helps you spot them faster in future code.

SECTION 8

Connection to Advanced Testing & Defensive Coding

The boundary testing skills you are learning now form the foundation of professional software testing practices. As you advance in computer science, these ideas expand into formal methodologies used in industry and academia alike.

What You Learn NowAdvanced Version
Manually testing boundary cases (empty, one element, first/last)Boundary value analysis — a formal testing technique used in software quality assurance
Checking loop behavior with print statementsUnit testing frameworks (like Python's unittest or pytest) that automate these checks
Adding guard clauses for empty listsDefensive programming and assertions that validate all function inputs
Mentally tracing what a loop does on each iterationLoop invariants — formal proofs that a loop is correct, used in algorithm design courses
Counting iterations with the range formulaBig-O complexity analysis — understanding how loop bounds affect runtime performance

How beginner loop skills scale into professional practice

In AP Computer Science and college-level courses, you will encounter nested loops, recursive calls, and complex data structures where boundary bugs become even more subtle. The habit of always testing boundaries first will save you hours of debugging throughout your programming career. Many professional developers consider boundary testing the single most valuable debugging skill they learned early on.

SECTION 9

Practice Problems

Question 1: CONCEPTUAL

Explain in your own words why range(0, 5) produces five values (0, 1, 2, 3, 4) instead of six. How does Python's exclusive upper bound design help prevent off-by-one errors?
Show answer
Python's range(0, 5) includes the start value 0 but excludes the stop value 5, giving exactly 5 − 0 = 5 values. This design is intentional: when you want to iterate over a list of length n, you write range(n) or range(0, n), and you get exactly n iterations. You never need to subtract 1 from the length, which eliminates the most common source of off-by-one errors. The formula iterations = stop − start makes it easy to verify your loop count.

Question 2: BASIC CALCULATION

How many times does the following loop body execute? What values does i take? for i in range(3, 10, 2): print(i)
Show answer
Using the iteration count formula: (10 − 3) ÷ 2 = 3.5, rounded down to 4 iterations. The variable i takes the values 3, 5, 7, and 9. Note that 10 is excluded (exclusive upper bound), and the step of 2 means only odd numbers from 3 to 9 are produced. A common mistake would be to think i reaches 10, but it stops at 9 because 9 + 2 = 11, which exceeds the stop value of 10.

Question 3: INTERMEDIATE

The following function is supposed to return the sum of all elements in a list, but it has a bug. Identify the bug, explain what boundary case reveals it, and write the corrected code. def sum_list(numbers): total = 0 i = 1 while i <= len(numbers): total += numbers[i] i += 1 return total
Show answer
There are two bugs. First, i starts at 1, so numbers[0] (the first element) is never added to the total — this is revealed by testing a single-element list like [5], which returns 0 instead of 5. Second, the condition i <= len(numbers) causes an IndexError when i equals len(numbers), because valid indices go from 0 to len(numbers) - 1. The corrected code: i = 0 and while i < len(numbers):. Alternatively, use for num in numbers: to avoid index management entirely.

Question 4: APPLIED

You are writing a program that prints a numbered list of student names. The output should look like: 1. Alice 2. Bob 3. Charlie Write a loop that works correctly for any list of students, including an empty list (which should print nothing). Then list three boundary test cases you would use and what the expected output should be for each.
Show answer
A correct solution uses for i in range(len(students)): and prints f"{i + 1}. {students[i]}". The i + 1 converts from zero-based indexing to human-readable 1-based numbering. Three boundary tests: (1) empty list [] — should print nothing; (2) single element ["Alice"] — should print "1. Alice"; (3) check that the last student in a multi-element list gets the correct number (e.g., for 3 students, the last line says "3. Charlie", not "2. Charlie"). An alternative Pythonic solution uses enumerate(students, start=1).

Question 5: CRITICAL THINKING

A classmate writes a function to check if a list is sorted in ascending order. They claim it works because they tested it with [1, 2, 3, 4, 5] and [5, 3, 1]. def is_sorted(lst): for i in range(len(lst)): if lst[i] > lst[i + 1]: return False return True Explain why their testing was insufficient. Identify the bug, describe which boundary case would reveal it, and provide a complete corrected version that handles all edge cases.
Show answer
The classmate's tests only used "typical" inputs and missed boundary cases. The bug is that range(len(lst)) makes i go up to len(lst) - 1, and then lst[i + 1] accesses lst[len(lst)], which is out of bounds — an IndexError. This is revealed by any non-sorted list or any sorted list, because the loop always reaches the last index. The fix is range(len(lst) - 1) so that i + 1 never exceeds the last valid index. Additionally, an empty list or single-element list should return True (already sorted), which the corrected loop handles since range(0) and range(0) skip the loop body. Corrected: for i in range(len(lst) - 1):. The lesson: always test with the boundary case that exercises the loop's last iteration.

SUMMARY

Lesson Summary

Summary

Off-by-one errors occur when a loop iterates one time too many or too few, typically caused by confusing inclusive and exclusive bounds. Python's range(start, stop) function includes start but excludes stop, producing exactly stop − start iterations. The most common mistakes include subtracting 1 when range already excludes the stop value, starting at 1 instead of 0, and using <= instead of < in while loops.

The best defense against these bugs is systematic boundary testing: always test with an empty input, a single-element input, and inputs where the answer depends on the first or last element. The fencepost principle reminds us that the count of items between boundaries differs from the count of boundaries themselves. By building the habit of testing edges before trusting your loop, you will catch the vast majority of loop bugs before they reach your users.

Varsity Tutors • Computer Programming 1 (Python) • Avoiding Loop Bugs