Python range() Function: How-To Tutorial With Examples

The Python range() function can be used to create sequences of numbers. The range() function can be iterated and is ideal in combination with for-loops. This article will closely examine the Python range function:

  • What is it?
  • How to use range()
  • How to create for-loops with the range() function

I’ll also show you how to use ranges in for-loops to create any loop you want, including:

  • Regular loops over an increasing range of numbers
  • Loops that go backward (e.g., from 10 to 0)
  • Loops that skip values

What is the Python range function?

Python’s range acts as a built-in function, and is commonly used for looping a specific number of times in for-loops. Like many things in Python, it’s actually a Python type (or class), but when using it in a loop, we can treat it like a built-in function that returns an iterable object.

Range offers us a calculated sequence of numbers based on our input. There are three ways to call the range function:

# With one integer argument it
# counts from 0 to stop
range(stop)

# With two integer arguments it
# counts from start to stop
range(start, stop)

# With three integer arguments it
# counts from start to stop,
# with a defined step size
range(start, stop, step)

The arguments start, stop, and step are always integers. When we call range, it returns an object of class range. This range object can, in turn, provide a Python iterator, meaning we can loop (iterate) over its values. The range iterator calculates a new value on each call. This value is based on the start, stop, and step values. 

If this sounds difficult: it is. If you don’t fully understand iterators and iterability, you have two options at this point:

  1. Read on and see how to use ranges. I won’t blame you.
  2. Read up on iterators and iterables first, and then come back here.

If you want to learn Python properly, I advise you to do yourself a favor and pick option two.

How to use Python range

There are three ways to use range. We’ll look at all three in detail, starting with the simplest use case. To thoroughly understand the Python range function, I’ll show you exactly how a range works internally later on; it’s quite simple! However, it’s better to look at examples of how to use ranges first to get a better idea of what they are and are capable of.

Range with only a stop value

If we call range with one argument, that argument is the stop value. In this case, range will start counting at 0 until it reaches the stop value.

For example, if we call range(5), the created range object will return values 0, 1, 2, 3, and 4 when we iterate over it. So, important to note: range stops as soon as it reaches the stop value, and won’t return the stop value itself.

If that sounds confusing, a simple example will hopefully clarify it. Let’s use the range call in a for-loop:

Why does range not include the end value?

You might be wondering why range doesn’t include the end value. It all comes down to an important principle called zero-based indexing. Computers start counting at zero, while humans start counting at 1. When a computer needs to number 3 elements, it starts with element 0, then 1, and then 2. It’s the same with indexing. When accessing elements in a Python list, the first element resides at position 0. We would access it like this: my_list[0].

Because of this zero-based indexing, it’s easier if Python ranges start counting at zero too. E.g., if you want to loop through something using a range, you’ll probably want to start at element zero.

Range with a start and stop value

We don’t always want to start counting at zero, and that’s why we can also call range with both a start and stop value. A call to range(2, 5) will return the values 2, 3, and 4. Here’s another example:

Range with a positive step size

Finally, range() takes an optional argument that’s called the step size. The step size defines how big each step is between the calculated numbers. It’s best to illustrate this:

for i in range(0, 6, 2):
    print(i, end=" ")

# Output will be: 0 2 4

Each step skips a number since the step size is two. Let’s increase the step size:

for i in range(0, 101, 10):
    print(i, end=" ")

# Output will be:
# 0 10 20 30 40 50 60 70 80 90 100

Because we set stop to 101, the value 100 is also included in this range.

Range with a negative step size

Ranges can count backward as well. If we set the step size to a negative number, the range will count backward. If we want to count backward, we need to make sure that the value for start is larger than the value for end. Here’s an example:

for i in range(5, 0, -1):
    print(i, end=" ")

# Output will be:
# 5 4 3 2 1

We started at 5 and counted backward to 0. Because ranges don’t include the end value, 0 was not included. If you want to include 0, you need to set end to -1.

How a Python range works

Now that you’ve seen range in action, I’ll tell you how ranges work internally to understand them best. There’s not much magic to it!

A range is an iterable object, and this object can return an iterator that keeps track of its current state. Suppose we create a range with the call range(3). This returns an object of type range with our requested settings. Internally, a Python range will set the following internal variables:

  • the start value to 0,
  • the end value to 3,
  • and the step value to the default value of 1.

When we start iterating a range object, the range object will return an iterator. This iterator uses the values as defined above but has one extra: a counter variable i, that is initialized to 0 by default and increases on each iteration.

On each call to the iterator to get the next number, range looks at its internal state and calculates the next number to return. The next value is calculated like this:

next = start + step*i.

By default, range takes steps of size one:

  • So on the first call, the calculation that is made is this: 0 + 1*0 == 0. Range returns 0 and the counter i is increased by 1, making i == 1
  • On the 2nd call, it makes the same calculation with its current state: 0 + 1*1 = 1. It again increases the counter i to 2.
  • The 3rd call returns 2, since 0 + 1*2 == 2.
  • We’ve now reached the end of the range. As explained in my article on Python iterators, any following call to the iterator will return a StopIteration exception, signaling that there are no more items.

If you give your range a step size of 2 with range(0, 5, 2), you can repeat the calculations from above and see that the returned values are now: 0, 2, and 4.

Demonstrated with code

We can demonstrate this process with some code. In the example below, we first create a range object. Next, we manually request an iterator from the range object using the built-in iter() function. We start requesting the next value from it using another built-in function: next(). After 3 calls, you see that the iterable is exhausted and starts raising StopIteration exceptions:

my_range = range(0, 3)

# Obtain the iterable. In a loop, this manual
# step is not needed
my_iter = iter(my_range)

# Request values from the iterable

print(next(my_iter))
# 0

print(next(my_iter))
# 1

print(next(my_iter))
# 2

print(next(my_iter))
# throws a StopIteration exception

If you like, you can run this code for yourself too:

Python range: Iterable or iterator?

A range object is an iterable, meaning that it is an object that can return an iterator. As seen in the example above, we can obtain this iterator manually with the iter() function. When used in a for-loop, the iterator is requested automatically by the loop and all is taken care of.

The range object (the iterable) will return a new iterator on each call. So, although this is not how we typically use ranges, we can actually keep using a range object by requesting new iterators from it. To understand this in detail, read up on iterables and iterators.

Python range vs. list

Ranges offer one big advantage over lists: they require a constant, predictable amount of memory since they are calculated on the fly. All a range needs to keep track of are the start, stop, end, and iteration counter. So range(1000) requires the same amount of memory as range(1).

In contrast: a list with the values 0 to 999 takes a thousand times more memory than a list with just the value 1.

In Python 2 this wasn’t the case. The regular range would materialize into an actual list of numbers. At some point, xrange was introduced. In Python 3, it was decided that xrange would become the default for ranges, and the old materialized range was dropped. In addition, some extra features were added, like the ability to slice and compare ranges.

The name xrange does not exist in Python 3. If you encounter it, you’re likely looking at Python 2 code. I created an article on this subject if you want to convert it to Python 3 code.

Python Ranges in practice

What follows are some common and some less common operations on and with ranges that you might need in practice, besides using them for loops.

Convert range to list

We learned that ranges are calculated while lists are materialized. Sometimes you want to create a list from a range. You could do this with a for-loop. Or, more Pythonic and quicker: by using a Python list comprehension. However, the best way to do this is using the list() function, which can convert any iterable object to a list, including ranges:

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

Testing for membership

Ranges behave like a regular collection. Hence we can also test them for membership of a certain value. E.g. to test if a number is part of a range, you can do this:

my_number = 10
print(my_number in range(0, 11))
# True

Range slicing

Although I’m not sure you’ll ever need this (I never used it in practice), this feature is pretty cool. Just like you can slice lists and other objects with a sequence type, you can also slice a range object. When doing so, you might expect the range to materialize to a list. That’s not the case. Instead, a new range object is calculated and returned, as you can see in the following examples on the Python REPL:

>>> range(0, 5)[2:4]
range(2, 4)
>>> range(0, 5)[4:2:-1]
range(4, 2, -1)
>>> range(0, 10)[2:8:2]
range(2, 8, 2)

Comparing ranges

Finally, ranges can be compared as well. When comparing ranges, they are compared as sequences. You might expect that Python only checks if they are the same object. This used to be true until Python 3.3 came out. Since then, two different ranges that result in the same sequence will be seen as equal in Python.

Here are some examples on the REPL:

>>> range(0, 2) == range(0, 3)
False
>>> range(0) == range(4, 2)
True

In the last example, a range with start 4 and end 2 will result in a range of length zero; hence it is equal to a range(0).

Conclusion

You’ve learned about Python’s range and how to use it in a for-loop. Not only that, you’ve also learned how a range works internally. Besides these basics, you’ve learned how to convert a range to a list, that ranges can be compared, and tested for membership, and that we can even slice a range.

If you want to learn more about ranges, these resources might be of interest:

Get certified with our courses

Learn Python properly through small, easy-to-digest lessons, progress tracking, quizzes to test your knowledge, and practice sessions. Each course will earn you a downloadable course certificate.

Related articles

Leave a Comment