PythonThe range() Function

The range() Function

range() generates a sequence of numbers, and it's the tool you'll reach for most often when you need to loop a specific number of times — "do this 10 times," "count from 5 to 1," "process every third index." It comes in three forms depending on how many arguments you give it, and understanding how it behaves under the hood (it's lazy, not a list) will help you write more efficient loops.

range(stop) — counting from zero

With a single argument, range(stop) produces numbers starting at 0 up to (but not including) stop.

range(stop)

Python
for i in range(5):
    print(i)

# Output:
# 0
# 1
# 2
# 3
# 4
range(start, stop) — choosing a starting point

With two arguments, counting begins at start instead of 0, and still stops just before stop.

range(start, stop)

Python
for i in range(2, 7):
    print(i)

# Output:
# 2
# 3
# 4
# 5
# 6
range(start, stop, step) — controlling the increment

The third argument controls how much to add each time. It can skip values (a step greater than 1) or, with a negative value, count downward.

range(start, stop, step)

Python
for i in range(0, 20, 5):
    print(i)

# Output:
# 0
# 5
# 10
# 15

Negative step — counting down

Python
for i in range(10, 0, -1):
    print(i)

# Output: 10 9 8 7 6 5 4 3 2 1
# Note: stop (0) is excluded, and start (10) is included.

for i in range(5, -1, -1):
    print(i)
# Output: 5 4 3 2 1 0
stop is always exclusive
No matter which form you use, the `stop` value itself is never produced. `range(10, 0, -1)` stops just after `1`, not at `0` — it never actually reaches `0`.
range is lazy — it's not a list

A common misconception is that range(1000000) builds a million-item list in memory. It doesn't. range returns a special range object that only stores three numbers — start, stop, and step — and computes each value on demand as the loop asks for it. That's why it stays cheap no matter how large the range is.

range vs a real list, in memory

Python
import sys

r = range(1_000_000)
lst = list(range(1_000_000))

print(sys.getsizeof(r))     # 48 bytes — same for range(10) or range(10**9)
print(sys.getsizeof(lst))   # roughly 8,000,000+ bytes

Because range doesn't hand you a list directly, things like slicing or printing it won't show individual numbers unless you materialize it. Use list() when you actually need the values as a list — for printing, for passing to something expecting a list, or for reusing the same sequence multiple times.

Materializing a range into a list

Python
print(range(5))        # range(0, 5)  -- the object itself, not the numbers
print(list(range(5)))  # [0, 1, 2, 3, 4]
range() in a loop vs. a list comprehension

range() is usually paired directly with a for loop when you just need to repeat an action or need the index values themselves. A list comprehension like [x**2 for x in range(5)] is what you reach for when the goal is to actually build a new list of transformed values.

Looping vs. building a list

Python
# Looping: we just want to run some code 5 times, no list needed
for x in range(5):
    print(f"processing item {x}")

# Building a list: we want the *results*, collected
squares = [x**2 for x in range(5)]
print(squares)  # [0, 1, 4, 9, 16]

Goal

Idiomatic choice

Repeat an action N times / iterate over indices

for i in range(n):

Produce a new list of computed values

List comprehension: [f(x) for x in range(n)]

Need the sequence of numbers itself, reused multiple times

list(range(...)) stored in a variable

Tip
If you only need to loop over a range once, don't bother wrapping it in `list()` first — looping directly over the `range` object is both simpler and avoids allocating memory you don't need.