Kodeclik Blog
How to create an inclusive range in Python
One of the most vexing things for new Python programmers to grasp is to remember that the Python range function does not use an inclusive end value. While this is a common pattern with many other programming languages it can nevertheless be a source of confusion.
Consider for instance a very simple range function usage like so:
print(list(range(1,10,1)))
The output will be:
[1, 2, 3, 4, 5, 6, 7, 8, 9]
Note how the first argument (i.e., 1) denotes the starting value of the range, the third argument (also a 1) denotes the increment or step size, while the second argument (here, 10) indicates not the end value but one more than the end value. Essentially the way the range function operates is to (say it in English): “start at the start value, keep incrementing by the step size, but stop when you have reached the end value”. Thus, when the value 10 is reached we infer that we have exceeded the range and thus exit the range operation.
This can be confusing for new programmers because the starting value is inclusive but the ending value is not inclusive (i.e., it is exclusive). If you would like the ending value to be inclusive, you can write your own range function for use in your programs.
Here is how that might work:
def myrange(start, end, step):
return (range(start,end+1,step))
print(list(myrange(1,10,1)))
Note that we have defined a function called myrange that takes three arguments. We simply return a range with all these three arguments with one exception: we increment the end value (i.e., the second argument). In this manner, when we invoke myrange with arguments (1,10,1) we are essentially calling range with arguments (1, 11, 1).
If we run the above program we get:
[1, 2, 3, 4, 5, 6, 7, 8, 9, 10]
as expected.
You might be surprised to learn that the range function has optional arguments:
1. If three arguments are specified, they are interpreted as (start, end, stepsize) as before.
2. If two arguments are specified, they are interpreted as (start, end). I.e., stepsize is considered to be 1. In essence, this is the same as calling range with settings (start, end, 1).
3. If only one argument is specified, they are interpreted as (end), i.e., start is considered to be 0, and stepsize is considered to be 1. In essence, this is the same as calling range with settings (0, end, 1).
We can update our “myrange” function to support these capabilities as well. Let us update our myrange function as follows:
def myrange(start=0, end, step=1):
return (range(start,end+1,step))
print(list(myrange(1,10,1)))
When we execute this program, we get:
File "main.py", line 1
def myrange(start=0, end, step=1):
^
SyntaxError: non-default argument follows default argument
This is a complaint by Python that it needs us to list the default arguments after the non-default argument(s) (in this case, end). Here is an updated function:
def myrange(end, start=0, step=1):
return (range(start,end+1,step))
print(list(myrange(1,10,1)))
The output will be:
[]
Wow - what happened? Why is the range empty? This is because we have now flipped the order of arguments in myrange(). So the start value is considered to be 10, step size is 1, and the end value is 1. The start value is already higher than the end value! So the range is essentially empty!. The cleanest solution is to specify exactly which argument is what (and this way we can also retain the usual way in which we remember the range function):
def myrange(end, start=0, step=1):
return (range(start,end+1,step))
print(list(myrange(start=1,end=10,step=1)))
Note that the definition of the myrange function is the same (i.e., end is listed first since it is a non-default argument and the default arguments follow after) but when we invoke it, we are careful to specify which argument refers to which value. The output will be:
[1, 2, 3, 4, 5, 6, 7, 8, 9, 10]
as intended.
Let us try using myrange() with more possibilities of default arguments:
def myrange(end, start=0, step=1):
return (range(start,end+1,step))
print(list(myrange(start=1,end=10,step=1)))
print(list(myrange(start=1,end=10)))
print(list(myrange(end=10)))
print(list(myrange(10)))
The output is:
[1, 2, 3, 4, 5, 6, 7, 8, 9, 10]
[1, 2, 3, 4, 5, 6, 7, 8, 9, 10]
[0, 1, 2, 3, 4, 5, 6, 7, 8, 9, 10]
[0, 1, 2, 3, 4, 5, 6, 7, 8, 9, 10]
Note that in the first two cases we obtain the intended range because the default step size is 1 so whether we specify it or not the correct step size is used.
In the second two cases, the default start value is 0 (not 1) and as a result if we do not specify a start value of 1, we get an additional value of 0 in the range.
Finally, note that in the last invocation of myrange() we simply pass an argument of 10 and that is interpreted to be the ending value since that is the only non-default argument.
In summary, an inclusive range is a range of numbers that includes the endpoints. For example, an inclusive range with endpoints 1 and 20 would include all integers between 1 and 20, including 1 and 20 themselves. But usually ranges in Python exhibit the off-by-one property with respect to the ending value, i.e., they include the starting value but not the ending value. If this is disorienting you can create your own myrange() function (as we have done here) as a wrapper around the range function. (Or you can remember this quirk of Python whenever you use the range() function and be careful to work around this quirk to achieve your goals.)
For more Python content, checkout the math.ceil() and math.floor() functions! Also
learn about the math domain error in Python and how to fix it!
Interested in more things Python? Checkout our post on Python queues. Also see our blogpost on Python's enumerate() capability. Also if you like Python+math content, see our blogpost on Magic Squares. Finally, master the Python print function!
Want to learn Python with us? Sign up for 1:1 or small group classes.