Swiftorial Logo
Home
Swift Lessons
Tutorials
Learn More
Career
Resources

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.

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.