← Home

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]

Resources