Python range() in Depth

Master range(start, stop, step): boundaries, negative steps, empty ranges, slicing and why a range uses almost no memory.

Lesson 22 of 107Intermediate12 min read

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:

Python
for marker in range(2, 12, 3):
    print(marker)          # 2, 5, 8, 11

range() 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:

CallValues
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:

Python
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:

Python
import sys
big = range(10**12)
print(sys.getsizeof(big))     # 48 bytes on a typical 64-bit build
print(big[999_999_999_999])   # 999999999999

Calling 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:

Python
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))    # 4

Slicing 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.

Python
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.

Example 1

range() in Depth in a small program

Python
markers = range(2, 12, 3)
print(list(markers))
print(markers[-1])
print(list(range(5, 0, -2)))
Output
[2, 5, 8, 11]
11
[5, 3, 1]
Line-by-line explanation
  1. Line 1Start at 2, stop before 12, step 3.
  2. Line 2Converting to a list displays every value: 2, 5, 8 and 11.
  3. Line 3Negative indexing works on a range; the last value is 11.
  4. Line 4A negative step counts down: 5, 3, 1, stopping before 0.
Example 2

Trail markers in both directions

Python
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)))
Output
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
  1. Line 3Adding 1 to the stop includes the 10 km point.
  2. Line 4Stopping at -1 with a step of -2 includes 0 when counting down.
  3. 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

Don't
for km in range(1, 10):
    print(km)        # 1 to 9: the 10 km marker is missing
Do
for km in range(1, 11):
    print(km)        # 1 to 10

The stop is never produced. Add the step to it when you want the end point included.

Using range(len()) to read items

Don't
for i in range(len(trails)):
    print(trails[i])
Do
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.

Multiple choiceeasy

Which boundary is excluded by range?

Multiple choicemedium

Which review concern is valid for range() in depth?

range() in Depth Quiz

2 questions to check what you learned.

  1. Which boundary is excluded by range?

    • The start
    • The stop
    • Python ignores the distinction
  2. 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