The range() function generates a sequence of numbers, and it's the tool you'll reach for most often when you need a for loop to run a specific number of times. It's deceptively simple on the surface, but understanding exactly how its arguments work — and that it isn't actually a list — will save you from a lot of off-by-one bugs.
range() Arguments: start, stop, step
range() accepts up to three arguments. Used with just one argument, it generates numbers from 0 up to (but not including) that number:
for i in range(5):
print(i)
Output:
0
1
2
3
4
Notice 5 itself is never printed — the stop value is always exclusive. With two arguments, you control both the start and stop:
for i in range(2, 7):
print(i)
Output:
2
3
4
5
6
A third argument sets the step size — including negative steps, for counting backwards:
for i in range(0, 10, 2): # every second number
print(i, end=" ")
print()
for i in range(10, 0, -2): # counting down
print(i, end=" ")
Output:
0 2 4 6 8
10 8 6 4 2
range() Is Lazy — It's Not a List
A common misconception is that range() builds a full list of numbers in memory. It doesn't — it's a lazy sequence object that generates each number on demand as the loop asks for it. This makes range(10_000_000) essentially free in terms of memory, since it never materializes ten million integers at once.
r = range(5)
print(r) # shows the range object, not the numbers
print(list(r)) # convert explicitly to see the actual values
Output:
range(0, 5)
[0, 1, 2, 3, 4]
If you actually need a real list of numbers — say, to shuffle them or index into them repeatedly — wrap the call in list(), as shown above.
Common Use: Looping by Index
range(len(some_list)) is a frequent pattern when you need the index alongside the value, though enumerate() is usually the cleaner choice for that specific case. range() on its own is the natural tool any time you need to repeat an action a fixed number of times — see it used for exactly that in the Sieve of Eratosthenes program, which loops over a range to mark off multiples of each prime.
