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
Table of Contents
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:
- Read on and see how to use ranges. I won’t blame you.
- 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 counteri
is increased by 1, makingi == 1
- On the 2nd call, it makes the same calculation with its current state:
0 + 1*1 = 1
. It again increases the counteri
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:
- My article on Python iterators and iterability
- The official documentation on ranges
- This article by Trey Hunner, explaining in detail why range is not an iterator (but it is an iterable)