Python range: How It Works and When to Use It
python range: Understand Python's range object: how it generates sequences lazily, its memory efficiency, and when to convert it to a list.
When you need to iterate over a sequence of numbers, python range is the built-in tool that most developers reach for. But its behavior differs from a list in ways that matter for memory usage, performance, and correctness. This article explains how range works, how to use its parameters effectively, and where its lazy evaluation model is an advantage or a trap.
What range Actually Returns
range() does not return a list. It returns a range object, which is an immutable sequence type that computes its elements on demand. This is a critical distinction because it affects how you can use the result and how much memory it consumes.
numbers = range(5) print(type(numbers)) # <class 'range'> print(numbers) # range(0, 5)
The range object stores only the start, stop, and step values, along with the length. It does not store each integer. When you iterate over it, each value is produced one at a time. This is why range(10**9) is perfectly fine to create—it uses a constant amount of memory regardless of the number of elements.
Using start, stop, and step
The full signature is range(start, stop[, step]). The start parameter is inclusive, stop is exclusive, and step defaults to 1. The step can be negative to count downward.
# Basic usage list(range(5)) # [0, 1, 2, 3, 4] list(range(2, 8)) # [2, 3, 4, 5, 6, 7] list(range(0, 10, 2)) # [0, 2, 4, 6, 8] list(range(5, 0, -1)) # [5, 4, 3, 2, 1]
When step is positive, the sequence continues while the current value is less than stop. When step is negative, it continues while the current value is greater than stop. If step is zero, range raises a ValueError because an infinite sequence cannot be represented.
Lazy Evaluation and Memory Behavior
Because range is lazy, it does not allocate a list of all values. This is the primary reason it is memory-efficient for large ranges. Consider a loop that needs to iterate over a billion numbers:
for i in range(10**9): # process i pass
This loop runs without allocating a billion integers. Each i is generated on the fly and discarded after the iteration. If you used list(range(10**9)), you would need roughly 8 GB of memory just for the integer objects, plus the list overhead—likely causing a memory error on most machines.
The tradeoff is that a range object is not a list. You cannot index it with a slice that returns a list, and it does not support methods like append or extend. However, it does support indexing and membership tests efficiently because those operations are computed directly from the stored parameters.
r = range(0, 100, 5) print(r[2]) # 10 print(50 in r) # True print(r[1:3]) # range(5, 15, 5) - slicing returns a new range
Converting range to a List: When and Why
There are legitimate reasons to convert a range to a list. If you need to modify the sequence, pass it to a function that requires a list, or reuse it multiple times while preserving the exact values, a list is appropriate. The conversion is simple:
numbers = list(range(10))
But converting a large range defeats the memory advantage. Use a list only when the number of elements is small enough that memory is not a concern, or when the sequence must be mutable. For example, if you need to shuffle the numbers, a list is required because range is immutable.
import random numbers = list(range(20)) random.shuffle(numbers)
In most iteration scenarios, keeping the range object is better. It is also faster to iterate because it avoids the overhead of list indexing and the memory footprint of a large list.
Common Mistakes and Edge Cases
One common mistake is assuming range includes the stop value. It does not. Another is using a negative step without adjusting start and stop correctly. For instance, range(0, 5, -1) produces an empty sequence because the step direction does not match the start-to-stop direction.
print(list(range(0, 5, -1))) # [] print(list(range(5, 0, -1))) # [5, 4, 3, 2, 1]
Also, be aware that range supports only integer arguments. Passing floats raises a TypeError. If you need a sequence of floats, you must use a list comprehension or itertools functions like itertools.count with a float step.
# This raises TypeError # range(0.0, 1.0, 0.1) # Use a list comprehension instead floats = [i * 0.1 for i in range(10)]
Another edge case: when step is negative, the stop value is exclusive, and the sequence goes downward. The length of a range is computed as max(0, ceil((stop - start) / step)) for positive steps, and analogously for negative steps. This formula ensures that the sequence is finite and well-defined.
Performance and Operational Considerations
From a performance perspective, range is generally more efficient than a list for iteration because it avoids the memory allocation and the overhead of accessing list elements. The iteration itself is implemented in C and is very fast. However, if you need random access to many elements, a list may be faster because list indexing is a simple pointer dereference, while range indexing involves a multiplication and addition. In practice, the difference is negligible for most applications.
Operationally, one consideration is that range objects are hashable, while lists are not. This makes range usable as a dictionary key or set member, which can be useful for representing intervals or ranges of values.
r = range(10, 20) interval_map = {r: "ten to twenty"}
Another operational aspect is that range supports the len() function and membership tests in O(1) time, which is not possible for a list without scanning. This makes range a good choice for representing large numeric intervals that need frequent membership checks.
When to Choose Alternatives
While range is the standard tool for numeric sequences, there are cases where other constructs are better. If you need to iterate over indices and values of a sequence, enumerate is the right choice. If you need an infinite sequence, itertools.count provides a lazy counter. If you need to generate values with a non-integer step, a generator expression or numpy.arange (for numerical work) may be more appropriate.
# Using enumerate for index and value for idx, val in enumerate(["a", "b", "c"]): print(idx, val) # Infinite counter with itertools from itertools import count for i in count(10, 2): if i > 20: break print(i)
Choosing the right tool depends on whether you need a finite, integer-based sequence (range), a mutable collection (list), or a more specialized generator. Understanding the lazy nature of range helps you avoid unnecessary memory usage and write more efficient loops.