052. Range() Function
Generate integer sequences without allocating memory
052. Range() Function
Aryan’s RAM monitor needs to take snapshots at regular intervals — every 5 seconds for 60 seconds, or sample the last N readings. He reaches for range() everywhere:
import time
# Take 12 readings, 5 seconds apart
readings = []
for i in range(12):
mem = psutil.virtual_memory()
readings.append(mem.percent)
time.sleep(5)
# Report only the last 5 readings (indices 7 through 11)
for i in range(len(readings) - 5, len(readings)):
print(f" Reading {i}: {readings[i]:.1f}%")
# Count down to next alert check
for secs in range(10, 0, -1):
print(f"\rNext check in {secs}s...", end="")
time.sleep(1)Off-by-one errors in range() are subtle — range(n) gives 0 to n-1, not 0 to n. Aryan’s first draft used range(start, stop + 1) everywhere, which meant his “last 5 readings” loop over-ran the list by one index.
Memory-efficiency angle:
range(10_000_000)uses a fixed ~48 bytes regardless of size — it stores only start, stop, step. A list of 10 million integers would use ~80 MB. In a long-running monitor, this matters.
💡 Fun fact: In Python 2, range() returned an actual list of integers, which meant range(1_000_000) allocated millions of integers in memory. Python 3 changed range() to return a lazy range object — a mathematical sequence that generates values on demand. This was one of the biggest memory-efficiency improvements in Python 3 and a key reason why Python 3 handles large numeric sequences so much better than Python 2.
⚠️ Watch out: range(stop) is exclusive — range(5) gives 0, 1, 2, 3, 4, not 0, 1, 2, 3, 4, 5. The off-by-one error from writing range(n+1) when you meant range(n) (or vice versa) is one of the most common bugs in loop code. Always double-check boundary values with a quick mental test: what does the first and last iteration produce?
🤔 Think about it: range(n, 0, -1) counts down from n to 1 but excludes 0. If you needed to count down and include 0, what would you write? And why do you think the designers made stop exclusive rather than inclusive — what algorithmic or mathematical convention does this match?
Learning objectives
- Use range(n), range(start, stop), range(start, stop, step)
- Remember that stop is exclusive
- Use negative step for descending sequences
Key concepts
- range()
- sequences
- step
Try it
Concept detail
range(stop) → 0 to stop-1. range(start, stop) → start to stop-1 (stop is EXCLUSIVE). range(start, stop, step) → start, start+step, … up to but not including stop.
Common patterns: range(n) # 0, 1, …, n-1 range(1, n+1) # 1, 2, …, n (inclusive upper bound) range(0, n, 2) # 0, 2, 4, … (evens) range(n, 0, -1) # n, n-1, …, 1 (countdown, excludes 0) range(n-1, -1, -1) # n-1, n-2, …, 0 (reverse indices)
range() returns a range object — lazy, not a list. It stores only start, stop, step. range(10_000_000) uses ~48 bytes; list(range(10_000_000)) uses ~80 MB. len(range(5)) == 5. 5 in range(10) is O(1). list(range(5)) == [0, 1, 2, 3, 4]. range() with floats doesn’t work — use a list comprehension or numpy for that.
Solution
def sum_range(start, stop):
total = 0
for i in range(start, stop):
total += i
return total
def even_numbers(n):
return list(range(0, n, 2))
def countdown(n):
return list(range(n, 0, -1))Tests
def test_sum_range():
assert sum_range(1, 5) == 10 # 1+2+3+4
def test_sum_range_zero():
assert sum_range(0, 1) == 0
def test_sum_range_single():
assert sum_range(3, 4) == 3 # just the number 3
def test_even_numbers():
assert even_numbers(10) == [0, 2, 4, 6, 8]
def test_even_numbers_small():
assert even_numbers(3) == [0, 2]
def test_even_numbers_zero():
assert even_numbers(0) == []
def test_countdown():
assert countdown(5) == [5, 4, 3, 2, 1]
def test_countdown_one():
assert countdown(1) == [1]