Python - range() Function with Examples

The `range()` function is one of Python's most frequently used built-ins. It generates a sequence of integers, which makes it essential for controlling loop iterations, creating number sequences, and...

Key Insights

  • The range() function generates sequences of integers lazily, making it memory-efficient even for millions of numbers since it doesn’t store all values in memory at once.
  • Understanding the three-parameter form range(start, stop, step) unlocks powerful patterns like reverse iteration, skipping values, and creating arithmetic progressions.
  • While range(len(sequence)) works for index-based iteration, enumerate() is usually the better choice when you need both index and value.

Introduction to range()

The range() function is one of Python’s most frequently used built-ins. It generates a sequence of integers, which makes it essential for controlling loop iterations, creating number sequences, and generating indices.

Despite its simplicity, many developers only scratch the surface of what range() can do. They use range(10) in for loops and call it a day. But understanding its full capabilities—including negative steps, memory efficiency, and practical patterns—will make your code cleaner and more Pythonic.

Let’s break down everything you need to know about range().

Basic Syntax and Parameters

The range() function accepts one, two, or three integer arguments:

range(stop)              # start defaults to 0, step defaults to 1
range(start, stop)       # step defaults to 1
range(start, stop, step) # full control over the sequence

Here’s what each parameter does:

  • start: The first number in the sequence (inclusive). Defaults to 0.
  • stop: The end of the sequence (exclusive). This value is never included.
  • step: The difference between consecutive numbers. Defaults to 1. Cannot be 0.

The exclusive stop boundary trips up beginners constantly. range(5) produces 0 through 4, not 0 through 5.

# Single argument: stop only
print(list(range(5)))
# Output: [0, 1, 2, 3, 4]

# Two arguments: start and stop
print(list(range(2, 7)))
# Output: [2, 3, 4, 5, 6]

# Three arguments: start, stop, and step
print(list(range(0, 10, 2)))
# Output: [0, 2, 4, 6, 8]

Notice that I’m wrapping range() in list() to display the values. We’ll cover why shortly.

Using range() in For Loops

The primary use case for range() is controlling for loop iterations. When you need to repeat an action a specific number of times, range() is your tool.

# Execute code exactly 5 times
for i in range(5):
    print(f"Iteration {i}")

# Output:
# Iteration 0
# Iteration 1
# Iteration 2
# Iteration 3
# Iteration 4

You’ll also see range() used to iterate over list indices:

fruits = ["apple", "banana", "cherry", "date"]

for i in range(len(fruits)):
    print(f"Index {i}: {fruits[i]}")

# Output:
# Index 0: apple
# Index 1: banana
# Index 2: cherry
# Index 3: date

This pattern works, but it’s not ideal. Python provides enumerate() for this exact situation:

# Prefer this approach
for i, fruit in enumerate(fruits):
    print(f"Index {i}: {fruit}")

The enumerate() version is more readable and less error-prone. Reserve range(len()) for cases where you genuinely only need the index, or when you need to modify the list in place.

Working with Step Values

The step parameter transforms range() from a simple counter into a flexible sequence generator.

Counting by increments:

# Count by 2s
print(list(range(0, 10, 2)))
# Output: [0, 2, 4, 6, 8]

# Count by 5s
print(list(range(0, 50, 5)))
# Output: [0, 5, 10, 15, 20, 25, 30, 35, 40, 45]

# Count by 3s starting from 1
print(list(range(1, 20, 3)))
# Output: [1, 4, 7, 10, 13, 16, 19]

Reverse counting with negative steps:

Negative step values let you count backwards. When using a negative step, your start value must be greater than your stop value.

# Count down from 10 to 1
print(list(range(10, 0, -1)))
# Output: [10, 9, 8, 7, 6, 5, 4, 3, 2, 1]

# Countdown by 2s
print(list(range(10, 0, -2)))
# Output: [10, 8, 6, 4, 2]

# Reverse a range of indices
for i in range(4, -1, -1):
    print(i, end=" ")
# Output: 4 3 2 1 0

A common mistake is forgetting to adjust the start and stop when using negative steps:

# This produces nothing!
print(list(range(0, 10, -1)))
# Output: []

# You need start > stop for negative steps
print(list(range(10, 0, -1)))
# Output: [10, 9, 8, 7, 6, 5, 4, 3, 2, 1]

Converting range to Other Data Types

Here’s something that surprises many Python learners: range() doesn’t return a list. It returns a range object.

r = range(5)
print(r)
# Output: range(0, 5)

print(type(r))
# Output: <class 'range'>

This design is intentional. A range object is a lazy sequence—it calculates values on demand rather than storing them all in memory. This makes range() incredibly memory-efficient.

import sys

# A range object for 1 million numbers
r = range(1_000_000)
print(sys.getsizeof(r))
# Output: 48 bytes

# A list of 1 million numbers
lst = list(range(1_000_000))
print(sys.getsizeof(lst))
# Output: ~8,000,056 bytes (approximately 8 MB)

The range object uses the same 48 bytes whether it represents 10 numbers or 10 billion. The list grows linearly with the number of elements.

When you need an actual list or tuple, convert explicitly:

# Convert to list
numbers_list = list(range(5))
print(numbers_list)
# Output: [0, 1, 2, 3, 4]

# Convert to tuple
numbers_tuple = tuple(range(5))
print(numbers_tuple)
# Output: (0, 1, 2, 3, 4)

Range objects also support membership testing and indexing:

r = range(0, 100, 5)

# Check membership (very fast, O(1))
print(50 in r)  # Output: True
print(51 in r)  # Output: False

# Indexing
print(r[0])   # Output: 0
print(r[5])   # Output: 25
print(r[-1])  # Output: 95

# Length
print(len(r)) # Output: 20

Common Use Cases and Patterns

Let’s look at practical patterns you’ll encounter in real code.

Generating test data:

# Create a list of sequential IDs
user_ids = list(range(1001, 1011))
print(user_ids)
# Output: [1001, 1002, 1003, 1004, 1005, 1006, 1007, 1008, 1009, 1010]

# Generate test scores
test_scores = list(range(60, 101, 5))
print(test_scores)
# Output: [60, 65, 70, 75, 80, 85, 90, 95, 100]

Matrix iteration with nested loops:

# Create a 3x3 multiplication table
for i in range(1, 4):
    for j in range(1, 4):
        print(f"{i}x{j}={i*j}", end="\t")
    print()

# Output:
# 1x1=1   1x2=2   1x3=3
# 2x1=2   2x2=4   2x3=6
# 3x1=3   3x2=6   3x3=9

Iterating over parallel lists by index:

names = ["Alice", "Bob", "Charlie"]
scores = [85, 92, 78]
grades = ["B", "A", "C"]

for i in range(len(names)):
    print(f"{names[i]}: {scores[i]} ({grades[i]})")

# Output:
# Alice: 85 (B)
# Bob: 92 (A)
# Charlie: 78 (C)

For this pattern, zip() is usually cleaner:

for name, score, grade in zip(names, scores, grades):
    print(f"{name}: {score} ({grade})")

Reversing a list in place:

data = [1, 2, 3, 4, 5]
n = len(data)

for i in range(n // 2):
    data[i], data[n - 1 - i] = data[n - 1 - i], data[i]

print(data)
# Output: [5, 4, 3, 2, 1]

Creating arithmetic progressions:

# Fahrenheit to Celsius conversion table
for f in range(32, 213, 20):
    c = (f - 32) * 5 / 9
    print(f"{f}°F = {c:.1f}°C")

# Output:
# 32°F = 0.0°C
# 52°F = 11.1°C
# 72°F = 22.2°C
# ...

Summary

The range() function is deceptively powerful. Here’s what to remember:

Use range(stop) for simple iteration starting from zero. Use range(start, stop) when you need a custom starting point. Use range(start, stop, step) for increments, decrements, or skipping values.

Remember that range() returns a memory-efficient range object, not a list. Convert with list() or tuple() only when you actually need a concrete collection.

For iterating over sequences with indices, prefer enumerate() over range(len()). For parallel iteration, prefer zip(). Reserve range() for pure counting, index manipulation, or generating number sequences.

Master these patterns, and you’ll write cleaner, more efficient Python code.

Liked this? There's more.

Every week: one practical technique, explained simply, with code you can use immediately.