Table of contents
Text Link

Python range function and its methods

Why did the range() function feel like an operator in Python? Because it was always range-ing over things like an iterator but never seemed to do anything - it was more like a method acting! 

In the early days of Python, there was a simple and humble function called range(). This function generated a range of sequences of numbers and strings, with a start and stop value and an option of step value. A useful tool it was, but with limitations.

History overlook

In Python 2.x, the built-in function range() module was commonly used for simple terms of operations, i.e. to generate a string of numbers in a array. However, this approach could be memory-intensive for large ranges since it created a list of all the values in the range at once. Also the function worked only with integers. It didn't support the float type (i.e. floating-point numbers/float value in any of its arguments).

As Python language continued to grow in popularity and Python developers began working with increasingly large datasets, a new function called xrange in Python 2.X was introduced in 2002 as a more memory-efficient alternative. xrange() generates a range object that can be iterated over, but it does not consume memory for the entire range at once.

The xrange() function was particularly useful for types of tasks that involved iterating over large sequences of numbers, as it allowed developers to generate these sequences without running out of memory. Here is an example:

# Generate a range of sequence of integers starting from 0 to 999999
my_range = xrange(1000000)

# Iterate over the values in the range
for i in my_range:
    print(i)

As you can see, the usage of xrange() was quite similar to the traditional function, and the only difference was that it was more memory-efficient.

Although the xrange() function was functionally similar to the range() function, it had a much lower memory footprint. To illustrate the difference in memory usage between the two functions, consider this code snippet:

import sys

# using range()
nums1 = range(1000000)
print(sys.getsizeof(nums1))  # prints 9000112 (approx 9MB)

# using xrange()
nums2 = xrange(1000000)
print(sys.getsizeof(nums2))  # prints 40 (approx 40-byte string)

This code snippet shows that the range() function used approximately 9 megabytes of memory, while the xrange() function used only 40 bytes. This significant difference in memory usage made xrange() an excellent choice for wider ranges

 

Introduction to types generator functions in Python

Now you might be wondering, how did the xrange() function manage to use less memory than the original function in Python 2.x? What changed? The answer lies in the way that xrange() generates its sequence of numbers.

In Python 2.x, the xrange() function was introduced as a generator function. Unlike the original function which generated an entire list of numbers, xrange() generates numbers on-the-fly as they are needed, which means that it doesn't need to store the entire sequence in static memory storage.

Generator functions are a powerful feature of Python that allow you to generate a sequence of values on-the-fly as they are needed, rather than generating an entire list of values all at once. This can be incredibly useful for situations where you need to generate a large sequence of values, but you don't want to store the entire sequence in memory at once. In fact, the xrange() function in Python 2.x was implemented as a generator function. Generator functions have been a part of Python since version 2.2, which was released in 2001. They have continued to be an important feature of the language, and were further improved in Python 3.x with the introduction of the yield from statement. This allowed for more concise and efficient generation of sequences using nested generator functions.

Here's an example of a simple generator function in Python:

def count_up_to(n):
    i = 1
    while i <= n:
        yield i
        i += 1

This function, count_up_to(n), generates a sequence of integers starting from 1 up to and including n (an inclusive value). But notice that instead of returning the entire sequence at once, it uses the yield keyword to generate each value one at a time. By using the yield keyword to temporarily suspend the function's execution and return a inclusive value to the caller. The function can then be resumed later, with its local state preserved, to continue generating more values.

When you call count_up_to(n), it doesn't actually start generating values until you begin loop iteration over it (e.g. using a for loop with range or calling next() on the iterator range object).

Here's an example of how you might use this generator loop iteration in practice:

 for i in count_up_to(5):
    print(i)
# Output
# 1
# 2
# 3
# 4
# 5

The benefit of using a generator function like this is that it allows you to generate a potentially infinite sequence type without needing to store all iterators in static memory storage at once. It can also be more efficient, since you only generate the values that are actually needed, rather than generating the entire sequence upfront.

Memory efficiency

In Python 3.x, the xrange() function was renamed to range(), and the old function was removed. This was done to simplify the language and eliminate confusion, as the range() function now behaved like the xrange() function in Python 2.x.

But the evolution of the function did not stop there. In Python 3.x, the function was further optimised to allow for lazy evaluation of values. This means that the range() function generates values on the fly, as they are needed, rather than generating them all at once.

This lazy evaluation of values makes the function much more memory-efficient and faster than its predecessors. It also allows for the creation of infinite ranges, which can be iterated over without consuming excessive amounts of memory size.

Generator functions are extremely memory-efficient, as they only generate values on an as-needed basis. They can also be used to generate infinite sequences of values, such as the Fibonacci sequence.

 

Ease of use in Python code

The range() function is a versatile tool for generating sequences of integers. Its functionality can vary depending on the desired types of sequence, but most commonly it is used to generate a sequence of integers from 0 to n, as demonstrated in the following code:

for i in range(n):
    print(i)

However, the range() function can also be used to generate a reverse range of integers from n to 1. Before delving into this functionality, it is worth exploring the range() function in more detail, as the examples thus far only scratch the surface of its potential uses. The function can take one or more arguments with range. When only one argument is provided, the function generates a sequence of integers from 0 to the argument value. For example, to sum all numbers between 1000 and 5000, the following code example can be used:

for i in count_up_to(5):
    print(i)
# Output
# 1
# 2
# 3
# 4
# 5

While this code in Python works, it iterates over 5000 numbers when only 4000 are needed, which can be wasteful when dealing with larger datasets. To avoid this, a second single argument can be provided, specifying the starting value of the sequence. For example:

sum = 0
for i in range(1000, 5000):
    sum += i
print(sum)  # output = 9995000

This code iterates over 4000 numbers, starting at 1000 and ending at 4999. The same result is achieved more efficiently (with less iterators).

In addition, a third argument can be provided to specify the increment value of the sequence. Commonly the default value is 1. However, in the following example, the numeric built-in value is set to 2:

sum = 0
for i in range(1000, 5000, 2):
    sum += i
print(sum)  # output = 2497500

Here, the sequence is generated from 1000 to 4999 with an increment of 2. It only iterates over 2000 numbers, and the sum of the sequence is half of the previous example. To increment by a value other than 2, a third argument can be passed to the function specifying the desired increment value. For example:

sum = 0
for i in range(1000, 5000, 5):
    sum += i
print(sum)

This generates a sequence from 1000 to 4995 with increments in steps of 5.

The range() function is a powerful tool for generating sequences of integers. By providing one or more arguments, the sequence can be tailored to meet specific needs. This flexibility makes it a valuable Python function for a variety of programming tasks.

Fun assignment: If you haven't noticed it so far, let's see if you can notice it now.

sum = 0
for i in range(2, 10, 2):
    sum += i

# The output of the code is:
print(sum)

Did you guess it? If you did, then great, if you didn't then don't worry, it's not that big of a deal. The output of the above code is 20, which is the sum of all the even numbers between 2 and 10. Now some of you might wonder why 20 and why not 30? Because 2 + 4 + 6 + 8 + 10 = 30, well that's because the function doesn't include the upper limit, so it will only iterate over the numbers from 2 to 9 and not 10.

It's important to note that the function behaves differently depending on the number of optional arguments passed to it. If only one argument is passed, the sequence will start from 0. If two arguments are passed, the sequence will start from the first argument. If three are passed, the sequence will start from the first argument and increment by the value of the third argument. If no third argument is passed, the sequence will increment by 1. It's important to remember that if you want to start a sequence from 0 but increment it by a value other than 1, you still need to pass 3 arguments to the function. Otherwise, Python will assume that you only passed 2 arguments increments in steps by 1. These concepts may seem straightforward now, but as you continue reading this article, there may be more complex concepts that are not immediately obvious.

More Advanced Usage in Python

Now, let's dive deeper and explore the generator functions. We already looked at how generator functions work and how we can use them to generate a range of sequence of numbers efficiently and save a lot of memory size. But what if we want to create a list of numbers instead of just iterating over them? Fortunately, we can achieve that too, by using the list() function.

If you have some knowledge of lists in Python, you might have guessed by now that the list() function can be used to convert the range() function into a list of numbers. This opens up a whole new world of possibilities, allowing us to easily manipulate the sequence of numbers generated by the range() function as a list. With the list() function, we can access specific elements, sort the list, add or remove elements, and perform a variety of other operations that are available for lists in Python.

In summary, the range() function is not just limited to generating sequences of numbers; it can also be used to create lists of numbers using the list() function. Let’s look at an example for how to create a list using the range function (the output is in the square brackets).

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

But we are not limited to just the list function to create a list in python, we can also use list comprehension to do the same thing, which is a concise way of creating a new list based on an existing list or sequence. With list comprehension, we can create a new list by applying an expression to each element of the range object, without the need for a loop with range or lambda function. This technique is very useful when we need to manipulate an array and return array based on some criteria. But we are not gonna dive into it, for now let's see how we can create a list using list comprehension and range function with the following blocks of code.

numbers = [i for i in range(10)]
print(numbers)  # output = [0, 1, 2, 3, 4, 5, 6, 7, 8, 9]

That's great, we created a list of numbers but remember when I said that by using the range() function we can actually go from n to 1 instead of 0 to n. Well, it's just time to do that.

The range() function in Python has several optional parameters that allow us to generate a sequence of numbers in different ways. One such way is to create a reverse range. To do this, we can pass n (inclusive) as the first argument and 0 (exclusive) as the second step argument. This means that we will be generating numbers from n to 1, as 0 is not included in the sequence. Let's take a look at a code snippet that demonstrates this but at the same time I would really request you to try the next snippet on your own before you see the output.

for i in range(10, 0, -1):
    print(i)

Well that's weird, the expected output was from 10 to 1 but instead the program never ended or gave an error in the end or it started printing 10, 11, 12 and so on without a code warning. Now for those who were attentive, already know that by default the function increments in steps of i by 1 and we didn't pass the third argument to the function, so it's incrementing the value of i by 1 and it's never going to reach 0. So, how do we fix that? Well, it's actually very simple, we just need to pass a negative value of -1 as the third argument and it will start decrementing the value of i by 1. It's magic right, I know. Let's see how it works.

for i in range(10, 0, -1):
    print(i)

Perfect, now we are getting the output that we were expecting. But what if we want to go from 10 to 1 but we want to decrement the value of i by 2 instead of 1. Well, we can do the same thing like we did before with positive numbers, here we can just pass a negative integer of -2 as the third argument to the range() function and it will start decrementing the value of i by 2. And the code for that might look something like this:

for i in range(10, 0, -2):
    print(i)

Now that you know how to manipulate the range() function to generate sequences of numbers in various ways, what if you just need to know the length of a sequence without actually generating the sequence? In that case, you can use the len() function to get the length of the sequence.

It might seem counterintuitive at first, since we're not storing the sequence in memory, but because the range() function is a generator function, it actually knows the length of the sequence based on the starting and ending points and the increment or decrement value of i. When you use the len() function on a range object, it simply returns the length of the sequence, which is just a number associated with the sequence and not the sequence itself.

For example, if we want to know the length of a sequence that goes from 0 to 9, we can simply use the len() function on a range object that starts from 0 and ends at 10 (exclusive range):

length = len(range(10))
print(length)  # output = 10

The len() function in Python can come in handy when you need to know the size of a sequence without actually generating the entire sequence. It can save memory size and execution time, especially when working with large sequences.

Tips & Tricks

Here are some tips and tricks on using the Python range() function:

Using the range() function with enumerate()

One of the best ways to use the range() function is with enumerate(). enumerate() is a built-in Python function that takes an iterable (like a list, tuple, or range) and returns a new iterable that provides pairs of values: the index of each item in the original iterable, and the item itself. Here's an example:

fruits = ['apple', 'banana', 'cherry']
for i, fruit in enumerate(fruits):
    print(i, fruit)
# Output
# 0 apple
# 1 banana
# 2 cherry

You can use the range() function to generate the index numbers instead of using enumerate() on a list directly:

for i in range(len(fruits)):
    print(i, fruits[i])
# Output
# 0 apple
# 1 banana
# 2 cherry

However, using enumerate() is more Pythonic and usually easier to read.

Using the range() function with zip()

Another useful built-in function is zip(). zip() takes one or more iterables as arguments and returns a new iterable that provides tuples of values (an immutable sequence type), where the i-th tuple contains the i-th element from each of the input iterables. Here's an example:

names = ['Alice', 'Bob', 'Charlie']
ages = [25, 30, 35]
for name, age in zip(names, ages):
    print(name, age)
# Output
# Alice 25
# Bob 30
# Charlie 35

You can use the range() function in Python to iterate over the indices of the lists and access the values with the indices:

# Python
for i in range(len(names)):
    print(names[i], ages[i])
# Output
# Alice 25
# Bob 30
# Charlie 35

Again, using zip() is more Pythonic and usually easier to read.

Using the range() function with slicing a string in Python

You can also use the range() function with slicing to get a subset of a list. Here's an example block of code:

fruits = ['apple', 'banana', 'cherry', 'date', 'elderberry', 'fig']
for fruit in fruits[1:4]:
    print(fruit)
# Output
# banana
# cherry
# date

You can use the range() function to generate the indices of the slice:

for i in range(1, 4):
    print(fruits[i])
# Output
# banana
# cherry
# date

Using the range() function with a negative step value

Commonly default step value is a positive integer of 1, but you can specify a different type of value:

for i in range(0, 10, 2):
    print(i)
# Output
# 0
# 2
# 4
# 6
# 8

You can also use a negative step value (e.g. a negative range) to count down:

for i in range(10, 0, -2):
    print(i)
# Output
# 10
# 8
# 6
# 4
# 2

Using the range() function with a dictionary comprehension

You can also use the range() function with a dictionary comprehension to create a dictionary of values. Here's an example:

squares = {i: i * i for i in range(10)}
print(squares)
# Output
# {0: 0, 1: 1, 2: 4, 3: 9, 4: 16, 5: 25, 6: 36, 7: 49, 8: 64, 9: 81}

Using the range() function in interviews for Python developers

The range() function is a very common interview question for Python developers. Here's an example:

for i in range(1, 101):
    if i % 15 == 0:
        print('FizzBuzz')
    elif i % 3 == 0:
        print('Fizz')
    elif i % 5 == 0:
        print('Buzz')
    else:
        print(i)
# Output
# 1
# 2
# Fizz
# 4
# Buzz
# Fizz
# 7
# 8
# Fizz
# Buzz
# 11
# Fizz
# 13
# 14
# FizzBuzz
# ... (continues up to 100)

Conclusion

In conclusion, the range() function is a powerful tool in Python that allows us to generate a sequence of numbers efficiently. By using the various step parameters (step size, decimal step, default step), we can customize the sequence to fit our needs, whether it be changing the starting and ending points, incrementing or decrementing the values, or creating a reverse range.

Moreover, we can use the range() function in combination with other Python functions like list() and len() to generate a list of numbers or get the length of the sequence respectively. The range() function is also a useful tool in solving interview questions related to sequences and iterator variables.

With these tips and tricks in mind, you can utilize the range() function to its full potential and streamline your code to make it more efficient and readable. Keep practicing and experimenting with the range() function to become more comfortable with it and unlock its full potential in your Python projects.

Related Hyperskill topics

Share this article
Get more articles
like this
Thank you! Your submission has been received!
Oops! Something went wrong.

Create a free account to access the full topic

Wide range of learning tracks for beginners and experienced developers
Study at your own pace with your personal study plan
Focus on practice and real-world experience
Andrei Maftei
It has all the necessary theory, lots of practice, and projects of different levels. I haven't skipped any of the 3000+ coding exercises.