16.2 range(): Arguments, Memory Efficiency, and Pitfalls
The range() function is a cornerstone of Python’s loop constructs, particularly the for loop. It does not generate a list in memory; instead, it returns an immutable sequence type known as a range object. This object yields the next number in the sequence on demand, making it exceptionally memory-efficient, even for ranges representing extremely large spans of numbers. Its primary purpose is to provide a sequence of integers for controlling the number of times a for loop executes.
Arguments and Behavior
The range() function can be called with one, two, or three arguments, mirroring the slicing syntax.
range(stop): Generates integers from0up to, but not including,stop.for i in range(5): print(i) # Output: 0, 1, 2, 3, 4range(start, stop): Generates integers fromstartup to, but not including,stop.for i in range(3, 7): print(i) # Output: 3, 4, 5, 6range(start, stop, step): Generates integers fromstarttostop, incrementing bystep. Thestepcan be negative to create a descending sequence.for i in range(10, 0, -2): # Count down from 10 to 2 by 2s print(i) # Output: 10, 8, 6, 4, 2 for i in range(0, 10, 3): print(i) # Output: 0, 3, 6, 9
It is crucial to remember that the sequence generated is half-open: it includes the start value but excludes the stop value. This design choice aligns with Python’s zero-indexing and slicing semantics, making it easy to iterate over sequences by their indices (for i in range(len(my_list))).
Memory Efficiency: The Range Object
A common misconception is that range() returns a list. In Python 2, xrange() was the memory-efficient version, while range() built a full list. In Python 3, range() replaces xrange() and is always memory-efficient. To see the difference, compare the behavior of a large range to a list.
# This creates a range object that knows its parameters, consuming a tiny, fixed amount of memory.
large_range = range(1_000_000)
print(type(large_range)) # <class 'range'>
print(sys.getsizeof(large_range)) # A small value, e.g., 48 bytes
# This actually creates a list with one million integers, consuming significant memory.
large_list = list(range(1_000_000))
print(sys.getsizeof(large_list)) # A large value, e.g., 8448728 bytes
The range object calculates each value dynamically when it is needed in the iteration. This is an example of lazy evaluation. You can confirm its contents by explicitly converting it to a list (list(range(5))), but this defeats the memory efficiency for large ranges.
Common Pitfalls and Edge Cases
Understanding the nuances of range() prevents subtle bugs.
Off-by-One Errors: The most frequent pitfall is forgetting that the sequence does not include the
stopvalue.# This loop will run 5 times, not 6. for i in range(0, 5): print(i) # Prints 0,1,2,3,4 # To include the number 5, you must use range(0, 6)Empty Ranges: If the
startvalue is beyond thestopvalue (given a positivestep), or if thestartvalue is below thestopvalue (given a negativestep), the range will be empty. The loop will not execute.for i in range(5, 2): # No step provided, default step is +1. print("This will not print.") for i in range(2, 10, -1): # Wrong direction. print("This also will not print.")Floating-Point Steps: The
range()function only works with integers. You cannot use a float forstart,stop, orstep. To iterate over a floating-point sequence, use awhileloop or thenumpy.arange()function from the NumPy library.# This will raise a TypeError: 'float' object cannot be interpreted as an integer # for i in range(0.0, 5.0, 0.5): # print(i) # Correct alternative for floating-point ranges: start = 0.0 while start < 5.0: print(round(start, 1)) start += 0.5Negative Steps: When using a negative
step, the arguments must be chosen carefully. The sequence moves from a higherstartto a lowerstop.# This works correctly. for i in range(5, 0, -1): print(i) # Prints 5, 4, 3, 2, 1 # Note: It stops at 1, not 0. To include 0, use range(5, -1, -1)
Best Practices
- Use for Index-Based Iteration: The primary use case is iterating a specific number of times or over a sequence by index.
colors = ['red', 'green', 'blue'] for index in range(len(colors)): print(f"Color {index} is {colors[index]}") - Prefer Direct Iteration: If you don’t need the index, directly iterating over the sequence is more Pythonic.
# Less Pythonic for i in range(len(colors)): print(colors[i]) # More Pythonic for color in colors: print(color) - Use
enumerate()for Index and Value: When you need both the index and the value,enumerate()is the preferred tool overrange(len()).# Less Pythonic for i in range(len(colors)): print(f"{i}: {colors[i]}") # More Pythonic for index, color in enumerate(colors): print(f"{index}: {color}") - Leverage Memory Efficiency: Don’t hesitate to use
range(10**6); it’s cheap. Only convert it to a list (list(range(10**6))) if you genuinely need a mutable sequence of all integers simultaneously.