PYTHON • CONTROL FLOW

range() — Use range() correctly (start, stop, step) and avoid off-by-one errors

Master Python's range() function to write precise, bug-free loops every time.

Historical Context & Motivation

Before modern programming languages existed, early computer scientists had to manually manage how many times a loop would repeat. They would set up counter variables, increment them by hand, and check boundary conditions at each step. This process was tedious and extremely error-prone. The idea of a built-in function that generates a clean sequence of numbers emerged as languages evolved, and Python's range() function is one of the most elegant solutions to this problem.

1972
C Language and Manual Loops
The C programming language introduced the classic for(i=0; i<n; i++) loop structure, requiring programmers to manage start, stop, and step values manually.
1991
Python 1.0 — range() Is Born
Guido van Rossum released Python with a built-in range() function that returned a full list of integers, simplifying loop construction dramatically.
2001
Python 2.2 — xrange() Appears
To save memory, Python 2 introduced xrange(), a lazy version that generated values one at a time instead of building an entire list in memory.
2008
Python 3.0 — range() Becomes Lazy
Python 3 replaced the old list-based range() with the memory-efficient lazy version. The old xrange() was removed, and range() became the standard.

Despite how common range() is in everyday Python code, it remains a source of subtle bugs. The most infamous of these is the off-by-one error — getting one too many or one too few iterations because you forgot that range() excludes its stop value. Understanding exactly how range() works is essential for writing correct, predictable loops.

Core Principles & Definitions

The range() function produces an immutable sequence of integers. It accepts one, two, or three arguments: range(stop), range(start, stop), or range(start, stop, step). Grasping these three parameters — and especially the rule that the stop value is exclusive — is the key to avoiding off-by-one errors.

1

Start (Inclusive)

The first value in the sequence. Defaults to 0 when only one argument is given. The sequence includes this value.
2

Stop (Exclusive)

The boundary where the sequence ends. The sequence goes up to but never includes the stop value. This is the most common source of off-by-one errors.
3

Step (Increment)

The difference between consecutive values. Defaults to 1. Can be negative for counting backward, but must never be zero.
4

Half-Open Interval

Python's range uses a half-open interval written [start, stop). This mathematical convention means start is included and stop is excluded, which pairs naturally with zero-based indexing.
✦ KEY TAKEAWAY
Think of range() like a fence. If you want a fence with 5 posts starting at post 0, you write range(0, 5) — the 5 tells Python where to stop building, not the label of the last post. The last post is number 4, just like the last index in a 5-element list.

Visual Explanation — The Number Line

The following diagram shows exactly which values range() produces for three common calling patterns. Notice how the stop value is always marked with an open circle (excluded), while the start value is a filled circle (included). This visual pattern is your best defense against off-by-one errors.

Filled circles represent values that are included in the sequence. Open red circles mark the excluded stop value. Dashed circles show numbers that exist on the number line but fall before the start value.

The visual pattern is consistent across all three forms. In the first row, range(5) starts at 0 (the default) and counts up to but not including 5. In the second row, range(2, 7) shifts the starting point to 2 while still excluding 7. In the third row, range(1, 10, 2) skips every other number by using a step of 2, producing only odd values from 1 through 9. The stop value of 10 is never reached — it is merely the boundary.

How range() Generates Values

Under the hood, range() doesn't store every number in memory. Instead, it calculates each value on demand using a simple formula. Understanding this formula helps you predict exactly how many values a range will produce and what the last value will be.

NTH VALUE FORMULA
value(i) = start + i × step
Where i is the zero-based index (0, 1, 2, …). The sequence continues as long as value(i) < stop (for positive step) or value(i) > stop (for negative step).
LENGTH FORMULA
length = max(0, ⌈(stop − start) / step⌉)
The ceiling function ⌈ ⌉ rounds up to the nearest integer. If (stop − start) and step have opposite signs, the result is 0, meaning the range is empty.

Let's apply the length formula to range(2, 11, 3). We compute (11 − 2) / 3 = 9 / 3 = 3. The ceiling of 3 is 3, so this range produces exactly 3 values. Using the nth-value formula: value(0) = 2 + 0 × 3 = 2, value(1) = 2 + 1 × 3 = 5, value(2) = 2 + 2 × 3 = 8. Notice that 11 itself is never included because 2 + 3 × 3 = 11 meets the stop boundary.

💡 Why Exclusive Stop?
The exclusive stop convention aligns with zero-based indexing. A list with n items has valid indices from 0 to n − 1. Writing for i in range(n) gives you exactly those indices. It also makes splitting ranges clean: range(0, 5) and range(5, 10) together cover 0–9 with no overlap and no gap.

Common Off-by-One Errors & How to Avoid Them

An off-by-one error (sometimes abbreviated OBOE) occurs when your loop runs one iteration too many or too few. These bugs are notoriously hard to spot because the code looks almost correct. The diagram below illustrates the most common mistakes side by side with their fixes.

Two of the most frequent off-by-one bugs: forgetting that range(n) starts at 0, and accidentally skipping index 0 by writing range(1, len(lst)) instead of range(len(lst)).
Common off-by-one scenarios and their fixes
GoalIncorrect range()Correct range()Why
Count 1 to nrange(n)range(1, n + 1)Default start is 0; stop must be n + 1 to include n
Count down from 5 to 1range(5, 0)range(5, 0, -1)Without a negative step, range cannot count backward
Even numbers 2–10range(2, 10, 2)range(2, 11, 2)Stop at 10 excludes 10; use 11 to include it
Iterate a list of length nrange(0, n + 1)range(n)n + 1 causes IndexError; range(n) naturally stops at n − 1

Worked Example — Summing Multiples of 3

Let's walk through a real problem step by step. The task is to compute the sum of all multiples of 3 from 3 to 30, inclusive.

Sum of Multiples of 3 from 3 to 30
1
Step 1 — Identify the Start ValueThe first multiple of 3 we want is 3. So we set start = 3.
start = 3
2
Step 2 — Determine the Stop ValueWe want to include 30 in our sum. Since range() excludes its stop, we need to set stop to one step beyond 30. Because our step is 3, we could use 31 (or any value from 31 to 33). The simplest and most readable choice is 30 + 1 = 31.
stop = 31
3
Step 3 — Set the Step ValueWe want every third number, so step = 3. Our range call becomes range(3, 31, 3).
range(3, 31, 3)
4
Step 4 — Verify the SequenceLet's convert to a list to check: list(range(3, 31, 3)) produces [3, 6, 9, 12, 15, 18, 21, 24, 27, 30]. That's 10 values, and 30 is correctly included. If we had used stop = 30 instead of 31, the list would end at 27 — a classic off-by-one error.
10 values from 3 to 30 ✓
5
Step 5 — Write the Complete CodeNow we write the full solution: total = sum(range(3, 31, 3)). The built-in sum() function adds all values in the range. The result is 3 + 6 + 9 + … + 30 = 165.
total = 165

range() vs. Other Iteration Tools

Python offers several ways to iterate, and range() is not always the best choice. Understanding when to use range() versus direct iteration or enumerate() will make your code both cleaner and less prone to errors.

Choosing the right iteration tool
ApproachBest ForLimitation
for i in range(len(lst))When you need the index number itself — e.g., to modify elements in place or compare adjacent itemsVerbose; easy to introduce off-by-one errors with start/stop
for item in lstWhen you only need the values — reading data, printing, accumulatingNo access to the index without extra code
for i, item in enumerate(lst)When you need both the index and the value simultaneouslySlightly more syntax to learn; start parameter can confuse beginners
while loop with counterWhen the number of iterations is unknown or depends on a runtime conditionMust manually manage the counter; risk of infinite loops
✦ KEY TAKEAWAY
A good rule of thumb: if you find yourself writing for i in range(len(lst)) and only ever using lst[i] inside the loop, you probably don't need range() at all. Use for item in lst instead — it's cleaner, faster, and eliminates any chance of an off-by-one error.

Connection to Advanced Iteration

As you grow as a Python programmer, you'll encounter concepts that build directly on the ideas behind range(). Generators, list comprehensions, and the itertools library all rely on the same principle of producing values lazily and using half-open intervals.

How range() concepts extend to advanced Python
ConceptConnection to range()When You'll Learn It
List ComprehensionsOften used with range() inside brackets: [x**2 for x in range(10)]Usually introduced alongside for loops in introductory Python
SlicingUses the exact same start:stop:step pattern as range(), and the stop is also exclusiveWhen working with lists and strings in depth
Generatorsrange() is itself a lazy iterable; generators extend this idea to custom sequences using yieldIntermediate Python or AP Computer Science A
NumPy arange() / linspace()arange() works like range() but with floats; linspace() includes the endpoint by defaultData science and scientific computing courses

One especially important connection is slicing. When you write my_list[1:5], you get elements at indices 1, 2, 3, and 4 — exactly the same values as range(1, 5). Mastering range() now gives you a head start on slicing, which you'll use constantly when manipulating strings and lists.

Practice Problems

PROBLEM 1 — CONCEPTUAL
Explain why range(5) produces 5 values but does not include the number 5 itself. What design principle makes this useful when iterating over a list?
PROBLEM 2 — BASIC CALCULATION
What output does list(range(3, 12, 2)) produce? How many elements are in the result?
PROBLEM 3 — INTERMEDIATE
Write a range() call that produces the countdown: 10, 8, 6, 4, 2. Then explain what would happen if you forgot the step argument.
PROBLEM 4 — APPLIED
You have a list of 20 student scores and you want to print every third score starting from the second one (index 1). Write a for loop using range() that does this correctly. What are the indices your loop visits?
PROBLEM 5 — CRITICAL THINKING
A classmate writes for i in range(1, n): total += i to sum all integers from 1 to n. They test it with n = 5 and get 10 instead of the expected 15. Diagnose the bug, fix the code, and explain a mental model that would prevent this mistake in the future.

Lesson Summary

Python's range() function generates an immutable sequence of integers using three parameters: start (inclusive), stop (exclusive), and step (increment). The single most important rule is that the stop value is never included in the output — this half-open interval convention aligns with zero-based indexing and makes range(len(lst)) produce exactly the right indices for any list.

To avoid off-by-one errors, remember the 'add one to include' rule: if you want the number B in your sequence, set the stop to B + 1. For counting backward, always provide a negative step. When you don't need indices at all, prefer direct iteration (for item in lst) or enumerate() to write cleaner, less error-prone code. These same start/stop/step patterns will reappear in list slicing, comprehensions, and advanced libraries like NumPy.

Varsity Tutors • Python • range() — Use range() correctly (start, stop, step) and avoid off-by-one errors