PYTHON / LOOPS
range() and numeric iteration
Build exact numeric sequences with range(), reason about its half-open bounds and step direction, and know why it is a lazy sequence rather than a list.
What you will learn
- Predict the values and length of any range(start, stop, step) call
- Use negative steps to count down instead of reversing a materialised list
- Exploit constant-time len(), indexing, slicing, and membership on range objects
- Produce fractional steps by iterating over integers and dividing
Understanding range() and numeric iteration
range() accepts one, two, or three integers: range(5) means 0 up to 5, range(2, 5) means 2 up to 5, and range(2, 11, 3) adds a stride. The stop value is never produced. That half-open convention is what makes len(range(a, b)) equal to b - a, and it makes adjacent ranges tile without overlap: range(0, 3) and range(3, 6) together cover 0 through 5 with no value counted twice.
A range is not a list of numbers; it is a small object holding just start, stop, and step, computing each value as start + i * step when asked. That is why print(range(3)) shows range(0, 3) instead of numbers, and why range(10**18) costs the same memory as range(3). Because the values follow an arithmetic rule, range supports len(), indexing, slicing, and the in operator in constant time, solving for i arithmetically rather than scanning. Unlike a generator, a range is not consumed: you can iterate the same range object as many times as you like.
The step sign decides direction and emptiness. A range is empty whenever moving from start by step never reaches toward stop, so range(10, 0) is empty while range(10, 0, -1) yields 10 down to 1. Its length is max(0, ceil((stop - start) / step)), which is why range(0, 10, 3) has 4 elements even though 10 is not a multiple of 3. Every argument must be an integer; a step of 0.1 raises TypeError, so scale with integers and divide inside the loop.
r = range(2, 11, 3)
print(r)
print(list(r))
print(len(r), r[0], r[-1])
print(7 in r, 8 in r)
print(list(range(5, 0, -1)))
print(list(range(0, 5, -1)))A range is a lazily computed arithmetic sequence over a half-open interval, defined entirely by start, stop, and step.
Worked examples
Length and direction
Shows how the step sign and the excluded stop value determine both the contents and the length of a range.
for start, stop, step in [(0, 10, 3), (10, 0, -3), (5, 5, 1), (1, 10, 4)]:
r = range(start, stop, step)
print(f"range({start}, {stop}, {step}) -> len {len(r)} {list(r)}")Example explained
Line 1range(0, 10, 3) stops at 9 because the next value, 12, is past stop; 10 itself is excluded anyway.
Line 2With step -3 the range walks downward and stops before 0, so 1 is the last value produced.
Line 3range(5, 5, 1) is empty because start already equals stop, and len() reports 0 without any iteration.
Line 4len(r) is computed from the arithmetic formula, not by counting, so it is instant even for huge ranges.
Integers only, fractional steps by scaling
Demonstrates that range rejects float arguments and how to get 0.2 increments anyway.
try:
range(0, 1, 0.1)
except TypeError as e:
print("TypeError:", e)
print([i / 10 for i in range(0, 10, 2)])Example explained
Line 1range needs exact integer arithmetic to compute length and indices, so a float step is rejected outright.
Line 2The fix is to count in integer tenths with step 2 and divide once per value.
Line 3Dividing at the end keeps the iteration count exact, whereas repeatedly adding 0.1 would drift and could give one extra or one missing step.
Slicing, reversing, and equality
Shows that slicing a range gives another range and that two ranges are equal when they produce the same values.
r = range(0, 20, 5)
print(r[1:3])
print(list(reversed(r)))
print(list(range(15, -1, -5)))
print(r == range(0, 20, 5), range(0, 5, 7) == range(0, 3, 9))Example explained
Line 1r[1:3] returns a new range object, not a list, because a slice of an arithmetic sequence is still arithmetic.
Line 2reversed(r) knows start, stop, and step, so it walks backward without building an intermediate list.
Line 3The explicit range(15, -1, -5) gives the same values; note stop must be -1 to include 0.
Line 4range equality compares produced values, so range(0, 5, 7) and range(0, 3, 9) are equal since both yield just [0].
Important notes
range(0) and range(5, 5) are perfectly valid empty ranges; an empty range is not an error and loops over it just do nothing.
A range object can be iterated repeatedly, so reusing the same range variable in two loops works, unlike a generator which is exhausted after the first pass.
Common mistakes
Expecting stop to be included: range(1, 10) never yields 10, so a loop meant to run through 10 silently stops one short.
Counting down without a negative step: range(10, 0) is empty, so the loop body never executes and the bug shows up as missing output rather than an error.
Treating range as a list: print(range(3)) shows range(0, 3), and calling r.append(4) raises AttributeError because a range is immutable and computed on demand.
Try it yourself
Change, predict, then run
Print every multiple of 7 below 100 in descending order using one range with a negative step, then print len() of that same range and check it matches the number of lines printed.
Open the Python workspaceCheck your understanding
Testing 999_999 in range(0, 1_000_000, 3) returns almost instantly, and so does the same test on a range spanning a billion values. What does that reveal about range?
- range caches every value it has already produced, making repeated lookups fast
- range is a generator, and generators support fast lookup
- range solves start + i * step == value for a whole number i, so nothing is scanned
- range builds an internal set of its values when it is created
Show answer
Membership is decided by arithmetic on start, stop, and step, so the cost does not depend on how many values the range covers. Calling it a generator is tempting because range is lazy, but a generator must be consumed to be searched and cannot be indexed or measured with len(); range is a lazy immutable sequence, and it stores no values at all, which also rules out caching or an internal set.