You wrote a Python for loop with range(), but it either skipped the last element or crashed with an IndexError. You're not alone. I've debugged this exact problem hundreds of times—both in my own code and for students I've mentored. The frustrating part? The fix is usually a single character change.
The python for loop range combination is one of Python's most fundamental constructs, yet it trips up beginners and experienced developers alike. The half-open interval concept (where the stop value is excluded) is the root cause of most off-by-one errors you'll encounter. But once you understand how range() truly works, you'll write cleaner loops and spend less time debugging.
This guide covers everything: from the three forms of range() to advanced troubleshooting, performance optimization, and when to use alternatives. Let's fix those loops for good.
What Is Python range() and How Does It Work in a for Loop?
The range() function generates a sequence of integers. When combined with a for loop, it controls how many times the loop executes and what values the loop variable takes. Think of it as a number generator that creates numbers on demand—not all at once.
The Three Forms of range(): stop, start/stop, start/stop/step
range() comes in three flavors, each serving a different purpose:
range(stop) — The simplest form. It generates numbers from 0 up to (but not including) stop:
list(range(5)) # [0, 1, 2, 3, 4]
range(start, stop) — You specify both the starting point and the endpoint:
list(range(2, 7)) # [2, 3, 4, 5, 6]
range(start, stop, step) — Full control with a custom increment:
list(range(1, 10, 2)) # [1, 3, 5, 7, 9]
The key insight—and the source of most bugs—is that stop is always excluded. This is called a half-open interval. I remember spending an hour debugging a data processing script before realizing I'd written range(1, 100) instead of range(1, 101). That single missing number cost me real time.
Why range() Is a Lazy Sequence: Memory Efficiency Explained
Here's something that surprised me when I first learned Python: range(1000000) doesn't actually store a million numbers in memory. It's a lazy sequence—it generates each number only when you ask for it.
Compare this to a list:
r = range(1000000)
import sys
sys.getsizeof(r) # 48
l = list(range(1000000))
sys.getsizeof(l) # 8000072 (approximately)
That's a memory savings of over 99.99%. This lazy evaluation means range() objects are:
- Memory-efficient for large sequences
- Immutable—you can't change them after creation
- Reusable—you can iterate over the same range multiple times
In Python 2, range() returned a list (which could eat up memory), and xrange() was the lazy version. Python 3 simplified this by making range() lazy by default. If you're migrating old code, this is one change you'll appreciate.
Python for Loop range() Examples: From Basic to Advanced
Let's move from theory to practice. These examples cover the most common scenarios you'll encounter.
Iterating from 1 to 100: The Classic Use Case
The most frequent question I get from beginners: "How do I loop from 1 to 100?"
for i in range(1, 101):
print(i)
This prints 1, 2, 3, ..., 100. The range(1, 101) includes 1 (the start) but excludes 101 (the stop). So you get exactly 100 numbers.
An alternative approach that some developers prefer:
for i in range(100):
print(i + 1)
Which one should you use? I lean toward the first version—it's more explicit about what you're iterating over. The second version requires mental arithmetic every time you read it. But in performance-critical code, the difference is negligible.
Using range() with a Step: Counting by Twos, Backwards, and More
The step parameter opens up interesting possibilities:
Even numbers (positive step):
for i in range(0, 10, 2):
print(i) # 0, 2, 4, 6, 8
Descending even numbers (negative step):
for i in range(10, 0, -2):
print(i) # 10, 8, 6, 4, 2
Countdown from 5 to 0:
for i in range(5, -1, -1):
print(i) # 5, 4, 3, 2, 1, 0
Notice the pattern: with a negative step, start should be greater than stop. The loop still excludes stop, so range(5, -1, -1) gives you 5 down to 0 (not -1).
Looping Through a List with range() and Index
Sometimes you need the index, not just the value. This is common when modifying a list in place:
my_list = [10, 20, 30, 40, 50]
for i in range(len(my_list)):
my_list[i] = my_list[i] * 2
print(my_list) # [20, 40, 60, 80, 100]
But here's the thing—if you only need to read values, direct iteration is cleaner:
for item in my_list:
print(item)
I use range(len()) only when I need to modify elements or when the index carries meaning. Otherwise, direct iteration is more Pythonic and usually faster.
Python for Loop range() Not Working? Fix Common Errors
This section is born from real debugging sessions. These are the errors I've seen most often—and the fixes that work.
Index Out of Range Error: Why It Happens and How to Fix It
The classic off-by-one error. Here's what it looks like:
my_list = [1, 2, 3]
for i in range(len(my_list) + 1): # Bug: +1 creates 4 iterations
print(my_list[i])
Error:
IndexError: list index out of range
The traceback points to line 3. The problem? range(len(my_list) + 1) generates [0, 1, 2, 3], but my_list only has indices 0, 1, and 2.
The fix: Remove the + 1:
my_list = [1, 2, 3]
for i in range(len(my_list)): # Correct: 3 iterations
print(my_list[i])
A similar error happens when you try to access my_list[i+1] inside the loop. If you need to look ahead, adjust the range:
for i in range(len(my_list) - 1):
print(my_list[i], my_list[i+1])
Infinite Loop with range(): How to Avoid It
Here's a common misconception: range() itself cannot cause an infinite loop. It's a finite sequence. But I've seen developers create infinite loops by combining range() with a while loop incorrectly:
i = 0
while i < 10:
print(i)
i -= 1 # Bug: decrementing instead of incrementing
The fix is straightforward—use a for loop with range() instead:
for i in range(10):
print(i)
If you must use a while loop, add a break condition:
i = 0
while i < 10:
print(i)
i += 1
if i > 100: # Safety net
break
Can You Use Float in Python range()? No, and Here's What to Do Instead
Try this and you'll get an error:
for i in range(0.5, 2.5, 0.5): # TypeError!
print(i)
range() only accepts integers. This limitation frustrates many beginners. Here are two workarounds:
Option 1: While loop with float step
i = 0.5
while i < 2.5:
print(i)
i += 0.5
Option 2: NumPy's arange() (if you're using NumPy)
import numpy as np
for i in np.arange(0.5, 2.5, 0.5):
print(i)
I generally prefer the while loop for simple cases—it avoids adding a dependency. But for scientific computing, NumPy's arange() is more precise and faster.
Python for Loop range() vs. Alternatives: When to Use What
range() is powerful, but it's not always the best tool. Here's when to use alternatives.
range() vs. enumerate(): Getting Both Index and Value
Compare these two approaches:
fruits = ['apple', 'banana', 'cherry']
for i in range(len(fruits)):
print(f"{i}: {fruits[i]}")
for i, fruit in enumerate(fruits):
print(f"{i}: {fruit}")
Both produce the same output. But enumerate() is more readable and avoids the clunky fruits[i] syntax. I use enumerate() almost exclusively when I need both index and value.
When would I still use range()? Only when I need the index without the value—for example, initializing a list:
squares = [0] * 10
for i in range(10):
squares[i] = i ** 2
range() vs. while Loop: Performance and Readability
For fixed-iteration loops, for with range() wins on readability:
for i in range(1000):
process(i)
i = 0
while i < 1000:
process(i)
i += 1
Performance-wise, for loops with range() are generally faster. Python's internal optimizations make them more efficient than manually incrementing a counter. In a quick benchmark using timeit:
import timeit
for_time = timeit.timeit('for i in range(1000): pass', number=10000)
while_time = timeit.timeit('''
i = 0
while i < 1000:
i += 1
''', number=10000)
print(f"For loop: {for_time:.4f}s")
print(f"While loop: {while_time:.4f}s")
The for loop is typically 10-20% faster. But honestly, unless you're processing millions of iterations, readability matters more than this performance difference.
range() vs. zip(): Parallel Iteration Over Multiple Lists
When you need to iterate over multiple lists simultaneously, zip() is your friend:
names = ['Alice', 'Bob', 'Charlie']
scores = [85, 92, 78]
for name, score in zip(names, scores):
print(f"{name}: {score}")
for i in range(len(names)):
print(f"{names[i]}: {scores[i]}")
zip() stops at the shortest list. If your lists have different lengths and you need to handle that, range() gives you more control:
for i in range(max(len(names), len(scores))):
name = names[i] if i < len(names) else "Unknown"
score = scores[i] if i < len(scores) else 0
print(f"{name}: {score}")
Advanced Python for Loop range() Techniques: Step, Reverse, and Performance
Let's push beyond the basics.
How to Loop Backwards with range() Using a Negative Step
Two approaches for backward iteration:
for i in range(10, 0, -1):
print(i) # 10, 9, 8, ..., 1
for i in reversed(range(10)):
print(i) # 9, 8, 7, ..., 0
Notice the difference: range(10, 0, -1) starts at 10 and goes down to 1. reversed(range(10)) starts at 9 and goes down to 0. Which one you choose depends on whether you need to include 10 or 0.
I use reversed() when I'm working with an existing range object. I use negative step when I need precise control over start and end values.
Performance Optimization: When to Avoid range() in Loops
Here's a counterintuitive tip: direct iteration is faster than range(len()):
import timeit
direct = timeit.timeit('''
my_list = list(range(1000))
for item in my_list:
_ = item * 2
''', number=10000)
range_len = timeit.timeit('''
my_list = list(range(1000))
for i in range(len(my_list)):
_ = my_list[i] * 2
''', number=10000)
print(f"Direct: {direct:.4f}s")
print(f"range(len()): {range_len:.4f}s")
The direct iteration avoids the overhead of index lookups. For most code, the difference is negligible. But in tight loops, it adds up.
Even better: use list comprehensions when possible:
squares = [x**2 for x in range(1000)]
squares = []
for i in range(1000):
squares.append(i**2)
List comprehensions are both faster and more readable. I've refactored countless for loops into comprehensions and seen 2-3x speed improvements.
Using range() with List Comprehension and Generator Expressions
range() pairs beautifully with comprehensions:
squares_list = [x**2 for x in range(10)]
print(squares_list) # [0, 1, 4, 9, 16, 25, 36, 49, 64, 81]
squares_gen = (x**2 for x in range(10))
print(squares_gen) # <generator object <genexpr> at 0x...>
The generator version is memory-efficient for large ranges. If you're processing millions of numbers, use the generator:
for square in (x**2 for x in range(10_000_000)):
if square > 100:
break
print(square)
Frequently Asked Questions
What is range in Python for loop?
range() is a built-in function that generates a sequence of integers. It's most commonly used with for loops to repeat an operation a specific number of times. For example, for i in range(5): print(i) outputs 0, 1, 2, 3, 4. The range object is lazy—it doesn't store all values in memory, making it efficient for large sequences.
How to iterate from 1 to 100 in Python?
Use range(1, 101) in a for loop. The range(start, stop) function includes start but excludes stop, so range(1, 101) generates numbers 1 through 100. Here's the code:
for i in range(1, 101):
print(i)
This prints each number from 1 to 100 on a separate line.
How do you loop through a range in Python?
Use the for keyword followed by a variable name, in, and the range() function. The basic syntax is:
for variable_name in range(stop_value):
# code to repeat
The loop variable takes each value in the range sequentially. For example, for i in range(3): print(i) outputs 0, 1, 2.
What does 'for I in range 4' mean?
The correct syntax is for i in range(4). This creates a loop that iterates 4 times, with i taking values 0, 1, 2, and 3. range(4) is equivalent to range(0, 4). Here's a visual breakdown:
- Iteration 1:
i = 0 - Iteration 2:
i = 1 - Iteration 3:
i = 2 - Iteration 4:
i = 3
After the fourth iteration, the loop ends because the next value (4) equals the stop value, which is excluded.
How to use range() with a negative step?
Use the syntax range(start, stop, negative_step). For example, range(10, 0, -1) counts down from 10 to 1. The stop value is still excluded, and the step must be negative for descending order. Here's a practical example:
for i in range(10, 0, -1):
print(i) # 10, 9, 8, 7, 6, 5, 4, 3, 2, 1
Conclusion
Let's recap what we've covered. The python for loop range combination has three forms: range(stop) for counting from zero, range(start, stop) for custom start points, and range(start, stop, step) for controlled increments. The half-open interval (where stop is excluded) is the source of most off-by-one errors—but now you know how to handle it.
Common errors have straightforward fixes:
- Index out of range: Check your stop value, especially when using
range(len(list)) - Infinite loops: Use
forloops instead ofwhilefor fixed iterations - Float in range(): Use a
whileloop or NumPy'sarange()
For best practices:
- Prefer direct iteration over
range(len())when you only need values - Use
enumerate()when you need both index and value - Use
zip()for parallel iteration over multiple sequences - Consider list comprehensions for simple transformations
Now that you've mastered Python for loop range, try our interactive coding challenge below to test your skills! Or, if you're still stuck, leave a comment with your specific error—we'll help you debug it.