SECTION 1
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
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
range() function that returned a full list of integers, simplifying loop construction dramatically.2001 • Python 2.2 — xrange() Appears
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
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.
SECTION 2
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.
Start (Inclusive)
0 when only one argument is given. The sequence includes this value.Stop (Exclusive)
Step (Increment)
1. Can be negative for counting backward, but must never be zero.Half-Open Interval
KEY TAKEAWAY
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.SECTION 3
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.
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.
SECTION 4
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
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
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?
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.SECTION 5
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.
range(n) starts at 0, and accidentally skipping index 0 by writing range(1, len(lst)) instead of range(len(lst)).| Goal | Incorrect range() | Correct range() | Why |
|---|---|---|---|
| Count 1 to n | range(n) | range(1, n + 1) | Default start is 0; stop must be n + 1 to include n |
| Count down from 5 to 1 | range(5, 0) | range(5, 0, -1) | Without a negative step, range cannot count backward |
| Even numbers 2–10 | range(2, 10, 2) | range(2, 11, 2) | Stop at 10 excludes 10; use 11 to include it |
| Iterate a list of length n | range(0, n + 1) | range(n) | n + 1 causes IndexError; range(n) naturally stops at n − 1 |
Common off-by-one scenarios and their fixes
SECTION 6
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 Value
The first multiple of 3 we want is 3. So we setstart = 3.start = 3 - 2
Step 2 — Determine the Stop Value
We want to include 30 in our sum. Sincerange()excludes its stop, we need to set stop to one step beyond 30. Because our step is 3, we could use31(or any value from 31 to 33). The simplest and most readable choice is30 + 1 = 31.stop = 31 - 3
Step 3 — Set the Step Value
We want every third number, sostep = 3. Our range call becomesrange(3, 31, 3).range(3, 31, 3) - 4
Step 4 — Verify the Sequence
Let'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 Code
Now we write the full solution:total = sum(range(3, 31, 3)). The built-insum()function adds all values in the range. The result is 3 + 6 + 9 + … + 30 = 165.total = 165
SECTION 7
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.
| Approach | Best For | Limitation |
|---|---|---|
for i in range(len(lst)) | When you need the index number itself — e.g., to modify elements in place or compare adjacent items | Verbose; easy to introduce off-by-one errors with start/stop |
for item in lst | When you only need the values — reading data, printing, accumulating | No access to the index without extra code |
for i, item in enumerate(lst) | When you need both the index and the value simultaneously | Slightly more syntax to learn; start parameter can confuse beginners |
while loop with counter | When the number of iterations is unknown or depends on a runtime condition | Must manually manage the counter; risk of infinite loops |
Choosing the right iteration tool
KEY TAKEAWAY
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.SECTION 8
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.
| Concept | Connection to range() | When You'll Learn It |
|---|---|---|
| List Comprehensions | Often used with range() inside brackets: [x**2 for x in range(10)] | Usually introduced alongside for loops in introductory Python |
| Slicing | Uses the exact same start:stop:step pattern as range(), and the stop is also exclusive | When working with lists and strings in depth |
| Generators | range() is itself a lazy iterable; generators extend this idea to custom sequences using yield | Intermediate Python or AP Computer Science A |
| NumPy arange() / linspace() | arange() works like range() but with floats; linspace() includes the endpoint by default | Data science and scientific computing courses |
How range() concepts extend to advanced Python
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.
SECTION 9
Practice Problems
Question 1: CONCEPTUAL
range(5) produces 5 values but does not include the number 5 itself. What design principle makes this useful when iterating over a list?Show answer
range() function uses a half-open interval [0, 5), meaning it includes 0 and goes up to but excludes 5. This produces the values 0, 1, 2, 3, 4 — exactly five values. This design aligns perfectly with zero-based indexing: a list of length 5 has valid indices 0 through 4. By writing for i in range(len(my_list)), you automatically get every valid index without risking an IndexError. The exclusive stop also allows clean range splitting — range(0, 5) and range(5, 10) cover 0–9 with no overlap.Question 2: BASIC CALCULATION
list(range(3, 12, 2)) produce? How many elements are in the result?Show answer
[3, 5, 7, 9, 11], which contains 5 elements. Using the length formula: ⌈(12 − 3) / 2⌉ = ⌈9 / 2⌉ = ⌈4.5⌉ = 5, confirming our answer.Question 3: INTERMEDIATE
range() call that produces the countdown: 10, 8, 6, 4, 2. Then explain what would happen if you forgot the step argument.Show answer
range(10, 1, -2). This starts at 10, decrements by 2 each time, and stops before reaching 1. The values produced are 10, 8, 6, 4, 2. If you wrote range(10, 1) without the -2 step, the default step is +1. Since 10 is already greater than 1 and the step is positive, Python recognizes the range is empty and produces no values at all — no error, just an empty sequence.Question 4: APPLIED
range() that does this correctly. What are the indices your loop visits?Show answer
for i in range(1, 20, 3): print(scores[i]). Starting at index 1 with a step of 3, the visited indices are 1, 4, 7, 10, 13, 16, 19. The next would be 22, which exceeds the stop of 20, so the loop ends. Notice that using len(scores) instead of the hardcoded 20 would make the code more flexible: range(1, len(scores), 3). This avoids an off-by-one risk if the list size ever changes.Question 5: CRITICAL THINKING
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.Show answer
range(1, n) stops before n, so with n = 5 it produces 1, 2, 3, 4 (summing to 10, not 15). The fix is range(1, n + 1), which includes n. A reliable mental model: whenever your problem says 'from A to B inclusive,' always write range(A, B + 1). Think of the stop parameter as the 'first number NOT in the list.' If you want B in the list, the first number not in the list is B + 1. This 'add one to include' rule prevents the most common off-by-one error in Python.SUMMARY
Lesson Summary
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.