Back to Blog
Python

Python range vs list: Key Differences and Use Cases

python range vs list: Understand the technical differences between Python's range and list, including memory usage, iteration behavior, and when to choose each for eff...

pythonrangelistmemoryiterationperformance
Illustration comparing a compact range object with a large list of numbers, symbolizing lazy vs eager memory usage in Python.

In Python, range and list are both used for iteration, but they serve fundamentally different purposes. The choice between python range vs list often comes down to memory usage, mutability, and whether you need to to store the entire sequence in memory. This article explains the core differences, runtime behavior, and practical decision criteria for working developers.

The Core Difference Between range and list

range is a built-in type that represents an immutable sequence of numbers. It stores only the start, stop, and step values, and computes each element on demand when iterated. A list is a mutable, resizable array that stores all its elements in memory immediately.

r = range(10) l = list(range(10)) print(r) # range(0, 10) print(l) # [0, 1, 2, 3, 4, 5, 6, 7, 8, 9]

The range object does not reveal its contents when printed because it is a lazy sequence. It supports the same sequence protocol as a list—indexing, slicing, len(), and membership tests—but it does not support item assignment or resizing.

Memory Behavior: Lazy vs Eager

This is the most important practical distinction. A range object with a large stop value occupies a constant amount of memory because it only stores the parameters. A list created from that same range stores every integer as a Python object, which can consume significant memory.

import sys r = range(10**6) l = list(r) print(sys.getsizeof(r)) # small, typically 48 bytes print(sys.getsizeof(l)) # large, roughly 8 MB for 1 million pointers

The exact sizes depend on your Python version and platform, but the trend is consistent: range is O(1) memory, while list is O(n). For a million-element sequence, the list holds a million integer objects plus the list's internal array. The range object remains tiny.

This lazy evaluation makes range the natural choice for loops that need a numeric sequence without materializing it.

Iteration and Performance

Both range and list are iterable, and for loops over them behave identically in terms of iteration order. However, iterating over a range generates each number on the fly, whereas iterating over a list reads precomputed values from memory. For small sequences, the performance difference is negligible. For large sequences, range avoids the upfront cost of allocating and initializing a list, which can be significant.

for i in range(1_000_000): pass # no list allocation for i in list(range(1_000_000)): pass # allocates a list first

The second loop first builds a list of one million integers, then iterates over it. The first loop never stores the numbers. If you only need to iterate once, range is almost always more efficient in both time and memory.

One subtle point: indexing into a range is O(1), just like indexing into a list, because the value is computed arithmetically. Slicing a range returns a new range object, not a list, which is also memory-efficient.

When to Use range

Use range when you need a sequence of integers for iteration, indexing, or as a numeric sequence that does not need mutation. Common scenarios include:

  • for loops with a known number of iterations
  • Generating indices for accessing other sequences
  • Creating arithmetic progressions with a custom step
  • Passing a lazy sequence to functions that accept iterables (e.g., sum(), max())
# Iterate with a step for i in range(0, 100, 5): print(i) # Use as an index source items = ["a", "b", "c"] for idx in range(len(items)): print(idx, items[idx])

Because range is immutable, it is also safe to share across multiple iterations without worrying about accidental modification.

When to Use list

Use a list when you need a mutable sequence that you will modify, reorder, append to, or store for later use. Lists support methods like append(), extend(), insert(), pop(), and sort(). If you need to keep the sequence around and change it, a range cannot do that.

# Mutable sequence needed squares = [] for n in range(10): squares.append(n**2) # Or more directly: squares = [n**2 for n in range(10)]

If you need random access and frequent updates, a list is the correct data structure. Also, when you receive a range and need to pass it to code that expects a list (e.g., some libraries), you can convert it with list(range_obj). But be mindful of the memory cost if the range is large.

Common Misconceptions and Edge Cases

One misconception is that range is a generator. It is not. A generator is an iterator that can only be consumed once, while range is a re-iterable sequence. You can iterate over the same range object multiple times.

r = range(3) print(list(r)) # [0, 1, 2] print(list(r)) # [0, 1, 2] again

Another edge case is that range supports negative steps, but the start and stop arguments must be ordered accordingly. For example, range(10, 0, -2) yields 10, 8, 6, 4, 2. This is a common source of off-by-one errors.

Also note that range is not a list, so it does not have list-specific methods. Trying to call r.append(5) raises an AttributeError. If you need to modify the sequence, convert to a list first.

Practical Decision Criteria

Choose range when:

  • You need a numeric sequence for iteration or indexing.
  • Memory efficiency matters, especially with large bounds.
  • You do not need to mutate the sequence.
  • You want to avoid the overhead of creating a list.

Choose list when:

  • You need to store, modify, or reorder the elements.
  • You need to pass the sequence to code that requires a list.
  • The sequence is small enough that memory is not a concern.
  • You need list-specific methods like append() or sort().

A common pattern is to use range for the loop and build a list only when you need to collect results. For example, a list comprehension over range produces a list of computed values, which is appropriate when the result set is small enough to hold in memory.

# Efficient: no intermediate list for i in range(10**6): process(i) # Sometimes necessary: collect results results = [process(i) for i in range(10**6)]

The second line materializes a list of one million results. If process returns large objects, that could be a problem. In such cases, consider using a generator expression instead of a list comprehension.

Compatibility and Version Considerations

In Python 2, range returned a list, and xrange was the lazy version. In Python 3, range behaves like the old xrange, and xrange no longer exists. This change is a common source of confusion for developers migrating from Python 2. If you see code that imports xrange, it will not work in Python 3.

Also, range supports arbitrary integer sizes, so range(2**100) is valid, though iterating over it would take an impractical amount of time. The range object itself is still tiny.

When working with floating-point sequences, range does not support floats. Use numpy.arange or a list comprehension with float division instead.

Final Technical Consideration: When Lazy Is Not Enough

There is one scenario where range can be less convenient: when you need to know the actual elements without iterating, such as for random access in a large sequence. Indexing range is O(1), but if you need to perform many lookups, a list might be faster because it avoids arithmetic computation per access. In practice, the difference is tiny for moderate sizes. Profile your code if you suspect a bottleneck.

Another consideration is that range is immutable, which makes it hashable? Actually, range objects are hashable in Python, but they are rarely used as dictionary keys. Lists are not hashable. This is a minor distinction but can matter in rare cases.

Ultimately, the decision between range and list is about whether you need a concrete, mutable collection or a lazy, immutable numeric sequence. Understanding this distinction helps you write code that is both memory-efficient and clear in its intent.

python range vs list: Key Differences and Use Cases | RYUSLOG DEV