What is range() in Python?

Discover the range() function in Python: syntax, parameters, practical examples and best practices for generating number sequences efficiently.
10 min read
Believemy logo

Definition of range() in Python

The range() function is a built-in Python function that generates an immutable sequence of integers. It is primarily used to iterate a defined number of times in a for loop, but its uses go far beyond that. If you want to master all the subtleties of Python, including range(), feel free to follow our complete Python course which covers this topic in depth.

Contrary to what you might think, range() does not return a list. It returns an object of type range, which is a lazy iterable. This means that the numbers are not all stored in memory simultaneously: they are generated on demand, which makes range() extremely performant, even for sequences of millions of elements.

Good to know

In Python 2, range() returned a complete list and xrange() was the lazy equivalent. In Python 3, range() behaves like the old xrange(), and xrange() no longer exists.

 

Syntax of range()

The range() function accepts one, two, or three arguments:

PYTHON
range(stop)
range(start, stop)
range(start, stop, step)

Here is the detail of each parameter:

ParameterDescriptionDefault value
startStarting value of the sequence (included)0
stopEnd value of the sequence (excluded)Required
stepIncrement step between each value1
Warning

Warning: the stop value is never included in the generated sequence. This is a frequent source of errors for beginners. For example, range(5) generates the numbers 0, 1, 2, 3, and 4 — but not 5.

 

Practical examples of range()

Using a single argument

When you pass a single argument to range(), it is interpreted as the stop value. The sequence then starts at 0:

PYTHON
# Generate numbers from 0 to 4
for i in range(5):
    print(i)

# Output:
# 0
# 1
# 2
# 3
# 4

This is the most common form of range(). You will often use it to repeat an action a specific number of times:

PYTHON
# Print "Hello" 3 times
for _ in range(3):
    print("Hello")

# Output:
# Hello
# Hello
# Hello

Here, the variable _ is a Python convention indicating that the iteration value is not used.

 

Using two arguments

By providing two arguments, you define the starting point and the stopping point of the sequence:

PYTHON
# Generate numbers from 2 to 7
for i in range(2, 8):
    print(i)

# Output:
# 2
# 3
# 4
# 5
# 6
# 7

Note that we passed 8 as the stop value to include the number 7 in the sequence.

 

Using three arguments (the step)

The third argument step allows you to control the interval between each number:

PYTHON
# Even numbers from 0 to 10
for i in range(0, 11, 2):
    print(i)

# Output:
# 0
# 2
# 4
# 6
# 8
# 10

You can also use a negative step to count backwards:

PYTHON
# Countdown from 10 to 1
for i in range(10, 0, -1):
    print(i)

# Output:
# 10
# 9
# 8
# 7
# 6
# 5
# 4
# 3
# 2
# 1
Warning

The step can never be equal to zero. Passing 0 as the step will raise a ValueError: range() arg 3 must not be zero exception.

 

Converting a range to a list or tuple

Even though range() is a lazy iterable, you can easily convert it to a list or a tuple when you need to manipulate the complete sequence:

PYTHON
# Convert to list
numbers = list(range(1, 6))
print(numbers)  # [1, 2, 3, 4, 5]

# Convert to tuple
numbers_tuple = tuple(range(1, 6))
print(numbers_tuple)  # (1, 2, 3, 4, 5)

This technique is very useful when you need to quickly create a list of numbers without writing them manually.

 

Using range() with enumerate()

Although enumerate is often preferred over range() for iterating over an existing sequence, both functions can be combined in certain cases:

PYTHON
# Use range to access specific indices
fruits = ["apple", "banana", "cherry", "date", "fig"]

# Iterate every other element
for i in range(0, len(fruits), 2):
    print(f"Index {i}: {fruits[i]}")

# Output:
# Index 0: apple
# Index 2: cherry
# Index 4: fig

Here, we combine range() with len to access list elements by their index, skipping every other element.

 

Using range() with list comprehensions

The range() function is a natural partner for list comprehensions to create transformed sequences:

PYTHON
# Squares of numbers from 1 to 10
squares = [x ** 2 for x in range(1, 11)]
print(squares)  # [1, 4, 9, 16, 25, 36, 49, 64, 81, 100]

# Multiples of 3 less than 30
multiples_of_3 = [x for x in range(0, 30, 3)]
print(multiples_of_3)  # [0, 3, 6, 9, 12, 15, 18, 21, 24, 27]

 

Checking membership in a range

One of the advantages of the range object in Python 3 is that you can test membership with the in operator in constant time (O(1)):

PYTHON
# Check if a number is in a range
print(5 in range(10))       # True
print(15 in range(10))      # False
print(4 in range(0, 10, 2)) # True (because 4 is even)
print(5 in range(0, 10, 2)) # False (because 5 is odd)

This operation is extremely efficient because Python doesn't need to traverse the entire sequence: it mathematically calculates whether the value belongs to the range.

 

Indexing and slicing a range

The range object supports indexing and slicing, just like a list:

PYTHON
r = range(0, 20, 3)

# Indexing
print(r[0])   # 0
print(r[3])   # 9
print(r[-1])  # 18

# Slicing (returns a new range)
print(r[2:5])     # range(6, 15, 3)
print(list(r[2:5]))  # [6, 9, 12]

# Length
print(len(r))  # 7

 

Properties and methods of the range object

The range object has three practical attributes that allow you to inspect its properties:

PYTHON
r = range(2, 20, 3)

print(r.start)  # 2
print(r.stop)   # 20
print(r.step)   # 3

These attributes are read-only. Since the range object is immutable (like a tuple or a frozenset), you cannot modify its properties after creation.

The range object also supports equality comparisons. Two range objects are considered equal if they generate the same sequence:

PYTHON
print(range(0, 10, 2) == range(0, 10, 2))  # True
print(range(0, 10, 2) == range(0, 12, 2))  # False

# Same sequence, different representations
print(range(0) == range(2, 1, 3))  # True (both are empty)

 

Advanced use cases

Generating reversed indices

To traverse a list backwards, range() combined with len is a classic approach:

PYTHON
colors = ["red", "green", "blue", "yellow"]

for i in range(len(colors) - 1, -1, -1):
    print(colors[i])

# Output:
# yellow
# blue
# green
# red
Good to know

To traverse a list backwards, the built-in reversed() function is often more readable: for color in reversed(colors). Use range() with a negative step when you explicitly need the indices.

 

Creating a custom function with range()

You can combine range() with def and yield to create custom generators:

PYTHON
def fibonacci_range(n):
    """Generate the first n Fibonacci numbers."""
    a, b = 0, 1
    for _ in range(n):
        yield a
        a, b = b, a + b

# Usage
for number in fibonacci_range(10):
    print(number, end=" ")

# Output: 0 1 1 2 3 5 8 13 21 34

 

Using with zip()

You can combine range() with zip to create custom index-value pairs:

PYTHON
names = ["Alice", "Bob", "Charlie"]
grades = [18, 15, 20]

results = {}
for i in range(len(names)):
    results[names[i]] = grades[i]

print(results)  # {'Alice': 18, 'Bob': 15, 'Charlie': 20}
Good to know

In this specific case, using zip(names, grades) would be more Pythonic. However, range() remains useful when you need the index for other operations.

 

Performance and memory

One of the main advantages of range() is its memory efficiency. Regardless of the size of the sequence, a range object always occupies the same amount of memory:

PYTHON
import sys

# Compare memory usage
small_range = range(10)
large_range = range(10_000_000)

print(sys.getsizeof(small_range))   # 48 bytes
print(sys.getsizeof(large_range))   # 48 bytes

# Compare with a list
small_list = list(range(10))
large_list = list(range(10_000_000))

print(sys.getsizeof(small_list))   # ~184 bytes
print(sys.getsizeof(large_list))   # ~80,000,056 bytes

As you can see, the range object only stores three values (start, stop, and step) and calculates each element on the fly, while a list must store each element individually in memory.

 

Best practices

Here are the best practices to follow when using range() in Python:

  • Prefer for item in collection over for i in range(len(collection)): when you don't need the index, iterate directly over the collection. It's more Pythonic and more readable.
  • Use enumerate when you need both the index AND the value: rather than for i in range(len(list)) followed by list[i], write for i, value in enumerate(list).
  • Don't call list(range(...)) unnecessarily: if you only need to iterate, use range() directly to save memory.
  • Use explicit variable names: instead of for i in range(10), prefer for attempt in range(10) if the context allows it.
  • Think of range() for validations: the membership test (in) on a range is O(1), making it ideal for checking if a number is within a given interval.

 

Limitations of range()

The range() function has some important limitations to be aware of:

  • Integers only: range() only supports integers. For floating-point number sequences, you can use numpy.arange() or create your own generator.
  • Zero step forbidden: a step of 0 raises a ValueError.
  • Linear sequence only: range() can only generate arithmetic progressions (with a constant interval).
PYTHON
# Error: range does not support floats
# range(0.0, 1.0, 0.1)  # TypeError!

# Solution: custom generator
def frange(start, stop, step):
    """Range for floating-point numbers."""
    while start < stop:
        yield round(start, 10)
        start += step

for x in frange(0.0, 1.0, 0.1):
    print(f"{x:.1f}", end=" ")

# Output: 0.0 0.1 0.2 0.3 0.4 0.5 0.6 0.7 0.8 0.9

 

Frequently asked questions

Question

Why doesn't range() include the stop value?

This is a Python convention inspired by zero-based indexing. When you write range(n), you get exactly n elements (from 0 to n-1). This behavior is consistent with list and string slicing in Python, where the end index is always excluded. It avoids "off by one" errors and makes code more predictable: range(len(list)) generates exactly the right indices to traverse a list.

 

Question

What is the difference between range() and list(range())?

range() returns a range object which is a lazy iterable occupying very little memory (about 48 bytes regardless of size). list(range()) forces the creation of a complete list in memory, which can consume a lot of resources for large sequences. Use range() directly when iterating, and convert to a list only if you need to modify the sequence or use it as a list.

 

Question

Can you use range() with negative numbers?

Yes, absolutely. You can use negative values for start, stop, and step. For example, range(-10, 0) generates numbers from -10 to -1, and range(10, -1, -1) counts down from 10 to 0. This is particularly useful for traversing indices in reverse order.

 

Question

How can I learn to use range() and Python effectively?

To master range() and the entire Python ecosystem, regular practice is essential. We recommend following our dedicated Python course on Believemy which will guide you step by step, from basics like range() to advanced concepts like generators and dataclass. You will find practical exercises and concrete projects to consolidate your knowledge.

Related terms

Discover our python glossary

Browse the terms and definitions most commonly used in development with Python.

Share this article

Want to help us? Share this article on your networks or even better: on your site, in an article or in your newsletter.