Fun with Learning Technology
LearnCoursesQuestionsTracksToolsNewsExplorePractice
Fun with Learning Technology

A new problem, explained clearly, every day.

Subscribe
Learn
  • Lessons
  • Topics
  • News
  • Tools
  • Courses
  • Career tracks
  • Everything
Site
  • About
  • Contact
  • Support
  • Privacy
  • Terms
Get the daily one

One email per new problem. No spam.

Request a tutorial

Requests shape what gets made next.

© 2026 Fun with Learning TechnologyRSS
Home›Courses›Python›The range() Function

Control Flow

The range() Function

The range() function is a built-in Python tool used to generate an immutable sequence of numbers, which is essential for controlling loop execution. Unlike creating a literal list in memory, range objects generate values lazily on demand, making them highly efficient for handling large iterations. You reach for range() whenever you need to perform an operation a specific number of times or iterate over indices of a collection.

Understanding the Basic Signature

At its core, range() acts as a generator for a sequence of integers starting from zero up to, but not including, the specified stop value. This 'exclusive' upper bound is a fundamental design philosophy in Python, ensuring that the total count of elements produced matches the integer provided as the argument. When you pass a single integer, say 5, the function generates the sequence 0, 1, 2, 3, 4. Understanding this is crucial because it aligns with zero-based indexing used throughout Python collections. Because range objects are lazy, they do not consume memory proportional to the size of the sequence; they calculate the next number only when the loop requests it. This behavior provides a significant performance advantage when you need to iterate over millions of items, as it avoids pre-allocating a massive list in memory before the loop even starts its first iteration.

# Generates integers from 0 to 4
for i in range(5):
    print(f"Current index: {i}")  # Prints 0 through 4 sequentially

Defining Start and Stop Bounds

While the single-argument version defaults to starting at zero, you can exert finer control by providing both a start and a stop value. By providing two arguments, you define the inclusive lower bound and the exclusive upper bound, allowing you to iterate through custom integer ranges that do not begin at zero. This flexibility is vital when you need to align your loop logic with specific data constraints, such as processing records starting from a specific offset or skipping initial metadata entries in a data structure. The logic remains consistent: the start value is included, and the stop value is excluded. If you set the start equal to the stop, the sequence remains empty, as there are no numbers that satisfy the criteria of being greater than or equal to the start while remaining strictly less than the stop value. This avoids common off-by-one errors during complex logic development.

# Iterate from 5 up to 9
for count in range(5, 10):
    print(f"Value: {count}")  # Starts at 5, stops before 10

Implementing Custom Step Increments

Beyond simple sequences, the range() function accepts a third argument known as the 'step'. This parameter dictates the increment between each generated integer in the sequence. By default, the step is 1, but changing it allows for powerful patterns, such as selecting every second item or counting backward. When providing a step, you must include a start and a stop value. If the step is positive, the function generates an increasing sequence; if negative, the start must be greater than the stop to generate a decreasing sequence. The efficiency of the range object remains intact here, as it performs simple arithmetic to calculate the next step rather than storing every integer. This is the idiomatic way to handle tasks like stepping through indices by a certain stride or reversing the iteration order without creating a reversed copy of a data structure, which saves significant processing time.

# Count backwards from 10 to 2 with a step of -2
for i in range(10, 0, -2):
    print(f"Countdown: {i}")  # Outputs 10, 8, 6, 4, 2

Memory Efficiency and Type Representation

A common point of confusion for newcomers is that range() does not return a list. Instead, it returns a range object, which is an immutable sequence type that represents the logic of the numbers without storing them. If you attempt to print a range object directly, you will see a description of its parameters rather than the values themselves. To view the contents as a list, you must explicitly convert the object using the list() constructor. This explicit conversion is a conscious choice by the developer: it forces you to acknowledge that you are about to allocate memory for all these integers at once. By defaulting to a lazy range object, Python protects you from accidentally exhausting system resources when dealing with sequences that span vast ranges of integers, ensuring that memory usage stays constant regardless of the total count of numbers.

# range object is lazy; list() materializes it into memory
number_range = range(0, 1000000)
print(type(number_range))  # Outputs: <class 'range'>
numbers = list(range(3))   # Forces creation of [0, 1, 2]

Practical Iteration Over Indices

In many real-world scenarios, you need to modify the contents of a list or process multiple lists simultaneously. Iterating directly over the items provides the values, but using range() in combination with len() provides access to the index, which is necessary if you need to perform modifications or reference neighboring elements. For example, if you are comparing a current item with its predecessor, having the index allows you to index into the sequence easily. This pattern is also essential when you need to iterate through two lists of equal length simultaneously by index. By using range(len(some_list)), you establish a bridge between the list size and the iteration logic, ensuring that your code is robust even when the data set changes size. This technique is a fundamental tool for building flexible and maintainable data processing loops in professional software development.

data = ['a', 'b', 'c']
# Use index to modify the list or access neighbors
for i in range(len(data)):
    print(f"Item at index {i} is {data[i]}")

Key points

  • The range function generates a sequence of integers starting from zero by default.
  • The stop value provided to range is always exclusive, meaning the sequence ends before reaching that number.
  • Range objects are lazy, which makes them highly memory efficient for large iteration counts.
  • You can provide a second argument to specify a custom starting point for the iteration.
  • The third argument, the step, allows you to skip values or count backwards through a range.
  • A range object does not store all its values in memory until you explicitly convert it to a list.
  • Using range combined with len() is the standard way to iterate over a list when you need access to the index.
  • The logic for range remains consistent, providing a predictable way to handle numerical iteration in Python.

Common mistakes

  • Mistake: Expecting range(10) to include the number 10. Why it's wrong: range() is exclusive of the stop parameter. Fix: Use range(11) or range(start, stop + 1) if you need to include the end value.
  • Mistake: Trying to use a float as an argument in range(). Why it's wrong: range() only accepts integers. Fix: Use a list comprehension or numpy.arange() if you need floating-point steps.
  • Mistake: Assuming range() returns a list directly. Why it's wrong: range() returns an immutable sequence object, not a list. Fix: Wrap it in list() if you need to view the full content or modify it.
  • Mistake: Forgetting that the third argument is the step, not an end index. Why it's wrong: range(start, step) interprets the second argument as the 'stop' value, leading to unexpected empty sequences. Fix: Use range(start, stop, step) explicitly.
  • Mistake: Attempting to use negative steps without a proper start and stop. Why it's wrong: If the start is less than the stop with a negative step, the range is empty. Fix: Ensure start > stop when using a negative step.

Interview questions

What is the primary purpose of the range() function in Python, and how does it behave in terms of memory usage?

The range() function in Python is a built-in constructor used to generate a sequence of numbers, which is essential for controlling loops. Its primary purpose is to provide a memory-efficient way to iterate over a range of integers. Unlike creating a full list in memory, range() produces an immutable sequence type that generates numbers on the fly as you iterate over them, making it highly efficient even for massive ranges like range(10**12).

Can you explain the parameters that the range() function accepts?

The range() function is versatile and accepts one, two, or three arguments. With one argument, range(stop), it generates numbers from 0 up to, but not including, the stop value. With two arguments, range(start, stop), it begins at the start value and ends before the stop value. With three arguments, range(start, stop, step), the function increments or decrements by the step value. For example, range(10, 0, -2) counts down from 10 to 2, excluding 0.

What happens if you try to print a range object directly in Python, and how would you view the contents of the range?

If you directly pass a range object to the print function, such as print(range(5)), Python will output 'range(0, 5)' rather than the numbers themselves. This occurs because range() is a lazy sequence type that does not store the full list of numbers in memory. To see the contents, you must cast it to a container type like a list using list(range(5)) or iterate through it using a for loop to access the individual elements.

Compare the approach of using 'for i in range(len(my_list))' versus 'for item in my_list' when iterating over a list.

While both approaches allow you to access elements, the 'for item in my_list' syntax is considered more Pythonic because it directly accesses the elements. In contrast, using 'for i in range(len(my_list))' is an index-based approach that requires an extra step to lookup the element using my_list[i]. You should choose the index-based approach only when you specifically need the index for tasks like modifying the list or referencing neighbor elements.

How can you use the range() function to iterate over a list in reverse order efficiently?

To iterate backwards, you can use the range() function with a negative step. By providing the length of the list as the start, 0 (exclusive) as the stop, and -1 as the step, you can traverse the indices in reverse. For example, 'for i in range(len(my_list) - 1, -1, -1):' allows you to access elements from the last index down to zero. This is a manual alternative to using the built-in reversed() function.

Is the range object in Python considered a mutable data type, and what are the implications of this for developers?

No, the range object is strictly immutable. This means that once a range object is created with its start, stop, and step parameters, it cannot be modified; you cannot change an individual value or resize the range. The implication is that if you need a mutable sequence, you must cast the range to a list. Immutability makes range objects thread-safe and allows them to be used as efficient keys in some advanced caching scenarios.

All Python interview questions →

Check yourself

1. What is the output of list(range(2, 8, 2))?

  • A.[2, 4, 6, 8]
  • B.[2, 4, 6]
  • C.[2, 3, 4, 5, 6, 7]
  • D.[2, 5, 8]
Show answer

B. [2, 4, 6]
The correct answer is [2, 4, 6]. range starts at 2, stops before 8, and steps by 2. Option 0 is wrong because it includes 8, which is the exclusive stop limit. Option 2 ignores the step of 2. Option 3 is mathematically incorrect based on the step logic.

2. Which of the following would produce the sequence [10, 8, 6]?

  • A.range(10, 6, -2)
  • B.range(10, 5, -2)
  • C.range(10, 7, -2)
  • D.range(10, 4, -2)
Show answer

B. range(10, 5, -2)
range(10, 5, -2) is correct because it starts at 10, moves by -2, and stops before 5 (stopping at 6). Option 0 stops at 8 (not including it). Option 2 stops at 8 (not including it). Option 3 produces [10, 8, 6, 4].

3. What happens if you run print(range(5, 2))?

  • A.It prints [5, 4, 3, 2]
  • B.It prints [5, 4, 3]
  • C.It prints range(5, 2)
  • D.It raises a ValueError
Show answer

C. It prints range(5, 2)
In Python 3, range() returns an object, so printing it directly displays the range signature. Options 0 and 1 are wrong because range returns the object, not a list. Option 3 is wrong because the range is empty (start > stop with default positive step).

4. Why does range(0, 10, 0) raise a ValueError?

  • A.Because the stop value is larger than the start value
  • B.Because range() does not support 0 as an argument
  • C.Because a step of zero would result in an infinite loop
  • D.Because the step must be a prime number
Show answer

C. Because a step of zero would result in an infinite loop
The step argument cannot be zero because it would cause an infinite repetition of the same value. Option 0 is incorrect as range() allows ascending sequences. Option 1 is false since 0 can be a start or stop. Option 3 is unrelated to the logic of the function.

5. What is the result of len(range(0, 100, 5))?

  • A.20
  • B.100
  • C.19
  • D.21
Show answer

A. 20
The range contains values 0, 5, 10... up to 95. This is 20 items. 100/5 = 20. Option 1 is the stop value, not the count. Option 2 and 3 are simple counting errors based on inclusive/exclusive off-by-one mistakes.

Take the full Python quiz →

← PreviousNested Loops and PatternsNext →Defining and Calling Functions

Python

78 lessons, free to read.

All lessons →

Track your progress

Sign in to mark lessons done, score quizzes and keep notes.

Open in the app