Python FAQ: Top Questions
7. What is the difference between `range()` and `xrange()`?
The distinction between `range()` and `xrange()` is a significant change between Python 2 and Python 3, reflecting an evolution towards more memory-efficient and performant iteration. In essence, `xrange()` is a Python 2-only function that was absorbed into `range()` in Python 3.
-
`range()` in Python 2:
- `range()` in Python 2 is an **eager** function. It generates a **list** of numbers immediately upon being called.
- This means that if you call `range(1000000)`, it will create a list in memory containing all one million numbers.
- For very large ranges, this can consume significant memory and be inefficient.
-
`xrange()` in Python 2:
- `xrange()` in Python 2 is a **lazy** (or generator-like) function. It generates numbers **on demand** as you iterate over them.
- Instead of creating a full list in memory, it returns an `xrange` object, which is an iterator. This object yields numbers one by one as they are requested.
- This makes `xrange()` highly memory-efficient, especially for large ranges, as it only stores the current number and the logic to generate the next one.
- `xrange()` is generally preferred over `range()` in Python 2 for loop iterations to save memory.
-
`range()` in Python 3:
- In Python 3, the `range()` function was redesigned to behave like Python 2's `xrange()`. It is now a **lazy** function and returns a **`range` object**, which is an immutable sequence type that yields numbers on demand.
- The `xrange()` function was completely **removed** from Python 3.
- Therefore, if you're writing code in Python 3, you should always use `range()`, and it will automatically provide the memory-efficient, lazy behavior that `xrange()` offered in Python 2.
This change in Python 3 simplified iteration by making the more memory-efficient approach the default for `range()`, aligning it with other lazy constructs like generators.
# --- Example 1: Python 2 (Conceptual - requires a Python 2 interpreter) ---
# Note: This code will NOT run in Python 3 as xrange is removed.
#
# print("--- Python 2 (Conceptual) ---")
# # range() in Python 2 creates a full list in memory
# list_range_py2 = range(5)
# print(f"range(5) in Python 2 type: {type(list_range_py2)}") # <type 'list'>
# print(f"range(5) in Python 2 values: {list_range_py2}") # [0, 1, 2, 3, 4]
# print(f"Memory used by range(1000000) in Python 2: (significant)")
#
# # xrange() in Python 2 creates an xrange object (iterator-like)
# xrange_obj_py2 = xrange(5)
# print(f"xrange(5) in Python 2 type: {type(xrange_obj_py2)}") # <type 'xrange'>
# print(f"xrange(5) in Python 2 values: {list(xrange_obj_py2)}") # [0, 1, 2, 3, 4] (list() forces evaluation)
# print(f"Memory used by xrange(1000000) in Python 2: (minimal)")
# --- Example 2: Python 3 (Current behavior) ---
print("--- Python 3 (Current Behavior) ---")
# range() in Python 3 is lazy and returns a 'range' object
# It does NOT create a list of numbers in memory directly.
range_obj_py3 = range(5)
print(f"range(5) in Python 3 type: {type(range_obj_py3)}") # <class 'range'>
print(f"range(5) in Python 3: {range_obj_py3}") # Output: range(0, 5) - not the full list
# To see the values, you need to iterate over it or convert it to a list:
print(f"Values from range(5) when iterated: ", end="")
for i in range_obj_py3:
print(i, end=" ")
print() # New line
list_from_range = list(range(5))
print(f"list(range(5)): {list_from_range}")
# Demonstrating memory efficiency in Python 3's range()
# This creates a range object, but doesn't allocate memory for all 10 million numbers.
# It's only generating them one by one as they are needed during iteration.
large_range = range(10000000)
print(f"Type of large_range: {type(large_range)}")
import sys
# The 'getsizeof' for range objects shows very small memory footprint,
# as it only stores start, stop, step values, not all numbers.
print(f"Memory size of range(10000000) object: {sys.getsizeof(large_range)} bytes")
# Compare with actual list (conceptual, don't run for very large numbers as it will crash)
# large_list = list(range(10000000))
# print(f"Memory size of list(range(10000000)): {sys.getsizeof(large_list)} bytes (much larger)")
Explanation of the Example Code:
-
**Python 2 (Conceptual):** The commented-out section shows how `range(5)` in Python 2 would produce a `
` and `xrange(5)` would produce a ` `. This highlights the explicit difference where `range` was eager (list-producing) and `xrange` was lazy (iterator-producing). The mention of memory consumption illustrates the practical problem `range` could cause with large inputs. -
**Python 3 (Current Behavior):**
-
In Python 3, `range(5)` directly returns a `
` object. When you print `range_obj_py3`, it doesn't show `[0, 1, 2, 3, 4]` directly; it shows `range(0, 5)`, indicating it's an object representing the range, not the generated list. - To get the actual numbers, you either iterate over the `range` object (as shown in the `for` loop) or explicitly convert it to a list using `list(range(5))`. This conversion then materializes all numbers into a list in memory.
- The `sys.getsizeof(large_range)` demonstrates the memory efficiency of `range()` in Python 3. Even for a range of 10 million numbers, the `range` object itself consumes minimal memory because it only stores the start, stop, and step values, generating numbers only when iterated upon. If you were to create a full list of 10 million integers, it would consume many megabytes of RAM.
-
In Python 3, `range(5)` directly returns a `
This example clearly demonstrates that Python 3's `range()` provides the memory-efficient, on-demand iteration behavior that was previously exclusive to `xrange()` in Python 2, making it the preferred and default choice for numerical sequences in modern Python.