range() produces a sequence of integers from a start value up to, but not including, a stop value, moving by a step. It's what you loop over when you need to repeat something a set number of times or count through positions:
for marker in range(2, 12, 3):
print(marker) # 2, 5, 8, 11range() takes one, two or three arguments: range(stop), range(start, stop) and range(start, stop, step). This lesson goes beyond the basics covered in the loops lesson: exact boundary rules, counting down, empty ranges, why a range of a billion numbers uses almost no memory, and when not to use range() at all. The examples schedule markers along a hiking trail.
Boundaries
The start is included and the stop is excluded:
| Call | Values |
|---|---|
range(5) | 0, 1, 2, 3, 4 |
range(2, 9) | 2, 3, 4, 5, 6, 7, 8 |
range(2, 9, 2) | 2, 4, 6, 8 |
range(1, 11) | 1 to 10 |
The half-open design has two practical benefits. len(range(a, b)) is simply b - a, and consecutive ranges join without overlap: range(0, 5) and range(5, 10) together cover 0 to 9 exactly once. It also matches indexing: range(len(items)) produces every valid index of a list.
To include an end value, add one to the stop: markers from 2 up to and including 8 are range(2, 9). All three arguments must be integers; range(0, 1, 0.5) raises TypeError.
Step direction
The step sets both the spacing and the direction:
print(list(range(0, 20, 5))) # [0, 5, 10, 15]
print(list(range(5, 0, -1))) # [5, 4, 3, 2, 1]
print(list(range(10, 0, -3))) # [10, 7, 4, 1]With a negative step, the range counts down and stops before reaching the stop value, so range(5, 0, -1) ends at 1. To count down to 0 inclusive, use range(5, -1, -1).
If the start is already past the stop in the direction of travel, the range is empty: range(5, 2) and range(0, 10, -1) produce nothing, and a loop over them simply doesn't run. No error is raised, which can hide a bug, so check your arguments when a loop unexpectedly does nothing. A step of zero is the one direct error: range(0, 10, 0) raises ValueError: range() arg 3 must not be zero.
Compact storage
A range doesn't store its numbers. It stores only start, stop and step, and calculates each value when it is needed. That's why range(10**12) is created instantly and is tiny:
import sys
big = range(10**12)
print(sys.getsizeof(big)) # 48 bytes on a typical 64-bit build
print(big[999_999_999_999]) # 999999999999Calling list(range(10**12)) would try to build all trillion numbers in memory and fail. Only convert a range to a list when you need to display it or change it; for loops, indexing and length checks, use the range directly.
A range is not an iterator: you can loop over the same range object as many times as you like, and len() works on it, unlike a generator.
Sequence tools
A range supports most read-only sequence operations, all calculated arithmetically:
markers = range(0, 30, 5)
print(len(markers)) # 6
print(markers[2]) # 10
print(markers[-1]) # 25
print(markers[::2]) # range(0, 30, 10)
print(15 in markers) # True, checked without looping
print(markers.index(20)) # 4Slicing a range returns another range. Membership testing for integers uses arithmetic, so it's fast even for huge ranges.
Even so, range() isn't the right tool for every loop. Looping over range(len(items)) just to get each item is un-Pythonic; loop over the list directly, use enumerate() when you need positions, and zip() to walk two lists together. Use range() when the numbers themselves matter: repetition counts, positions you calculate with, or numeric sequences.
Independent practice and a worked check
Task: create ascending and descending marker schedules and explain an empty range. Build markers every 3 km from 0 to 15 inclusive, the same markers in reverse, and show what range(15, 0, 3) produces.
up = range(0, 16, 3)
down = range(15, -1, -3)
print(list(up)) # [0, 3, 6, 9, 12, 15]
print(list(down)) # [15, 12, 9, 6, 3, 0]
print(list(range(15, 0, 3))) # []The last range is empty because a positive step moves upwards, but the start is already above the stop. For the worked check, the boundary that range() excludes is the stop; the start is always included when the range is not empty.
Reference and next practice
The documentation describes range objects and their sequence operations. Continue with lists and iterators and generators, which explain lazy sequences more generally. Practise with the Python exercises and Python quiz.
range() in Depth Summary
- Boundaries: range includes its start and excludes its stop.
- Step direction: A nonzero integer step determines spacing and direction.
- Compact storage: A range stores its defining parameters rather than a list of every generated integer.
- Sequence tools: Ranges support indexing, slicing, length and integer membership.
range() in Depth Examples
Each example shows the code, the output it produces and what each part does.
range() in Depth in a small program
markers = range(2, 12, 3)
print(list(markers))
print(markers[-1])
print(list(range(5, 0, -2)))[2, 5, 8, 11]
11
[5, 3, 1]Line-by-line explanation
- Line 1Start at 2, stop before 12, step 3.
- Line 2Converting to a list displays every value: 2, 5, 8 and 11.
- Line 3Negative indexing works on a range; the last value is 11.
- Line 4A negative step counts down: 5, 3, 1, stopping before 0.
Trail markers in both directions
trail_length = 10 # kilometres
outbound = range(0, trail_length + 1, 2)
inbound = range(trail_length, -1, -2)
print("Outbound:", list(outbound))
print("Inbound: ", list(inbound))
print("Markers:", len(outbound))
print("Is km 6 a marker?", 6 in outbound)
print("Every other marker:", list(outbound[::2]))
print("Empty:", list(range(trail_length, 0)))Outbound: [0, 2, 4, 6, 8, 10]
Inbound: [10, 8, 6, 4, 2, 0]
Markers: 6
Is km 6 a marker? True
Every other marker: [0, 4, 8]
Empty: []Line-by-line explanation
- Line 3Adding 1 to the stop includes the 10 km point.
- Line 4Stopping at -1 with a step of -2 includes 0 when counting down.
- Line 10A start larger than the stop with a positive step gives an empty range, without an error.
Common range() in Depth Mistakes
Expecting the stop value to be included
for km in range(1, 10):
print(km) # 1 to 9: the 10 km marker is missingfor km in range(1, 11):
print(km) # 1 to 10The stop is never produced. Add the step to it when you want the end point included.
Using range(len()) to read items
for i in range(len(trails)):
print(trails[i])for trail in trails:
print(trail)
for number, trail in enumerate(trails, start=1):
print(number, trail)range() in Depth in Real Projects
range() in practice
range() drives repetition with a fixed count, such as retrying a request three times, generating test data, or drawing a grid of rows and columns. It produces page offsets for pagination, range(0, total, page_size), and batch boundaries when processing a large list in chunks: for start in range(0, len(rows), 100): batch = rows[start:start + 100].
Because ranges are lazy and memory-light, they're also used to represent large numeric intervals in checks, such as if port in range(1024, 49152):. NumPy's np.arange() is the counterpart for arrays and, unlike range(), accepts decimal steps.
range() in Depth FAQ
Why does range() not include the last number?
Python uses half-open ranges so that len(range(a, b)) equals b - a, adjacent ranges don't overlap, and range(len(items)) produces exactly the valid indexes. To include the end value, add one to the stop.
How do I make range() go backwards?
Give a negative step and a start larger than the stop: range(10, 0, -1) counts 10 down to 1. Alternatively, reversed(range(1, 11)) produces the same values and is often easier to read.
Can range() use decimal steps?
No. All arguments must be integers. Loop over integers and divide, [i / 10 for i in range(0, 11)], or use NumPy's np.arange(0, 1.01, 0.1) or np.linspace(), which handle floats.
Does range() return a list in Python 3?
No. In Python 3, range() returns a lightweight range object that calculates values as needed. Python 2's range() built a full list, and xrange() was the lazy version. Use list(range(...)) when you actually need a list.
Why does my for loop with range() not run at all?
The range is probably empty: the start is already at or beyond the stop for the direction of the step, such as range(10, 1) with the default step of 1. Print list(range(...)) to see what it produces, and use a negative step to count down.
range() in Depth Exercises
Answer each one and check it instantly. If you get stuck, open the hint before the answer.
Which boundary is excluded by range?
Which review concern is valid for range() in depth?
range() in Depth Quiz
2 questions to check what you learned.
Which boundary is excluded by range?
- The start
- The stop
- Python ignores the distinction
Which statement correctly describes compact storage?
- A range stores its defining parameters rather than a list of every generated integer.
- Only the example output matters, not the rule
- Every operation has the same behavior