`range()` with optional inclusive `stop`

I’m considering proposing an optional inclusive parameter for the built-in range() function and would welcome feedback before drafting a formal PEP. This would be a change to the existing range() signature only—no new syntax or function.

Summary: When inclusive=True, the stop value would be included in the sequence. The parameter would be keyword-only and default to False, preserving current behavior.

Examples:

  • range(5, inclusive=True) → 0, 1, 2, 3, 4, 5

  • range(2, 6, inclusive=True) → 2, 3, 4, 5, 6

  • range(10, 0, -1, inclusive=True) → 10, 9, 8, 7, 6, 5, 4, 3, 2, 1, 0

Rationale: Inclusive ranges are common (e.g., “from 1 to 10” meaning 1 through 10). When teaching my nephew to code, he struggled with exclusive stop; a simple irange helper helped. As the Zen of Python says, “Explicit is better than implicit.” That experience made me think this could be useful more broadly. People currently use range(start, stop + 1); a built-in parameter would make that intent clear and reduce off-by-one mistakes.

Prior discussions: Inclusive range was discussed on python-ideas in 2010. The main proposal then was to change the default to inclusive (rejected as a breaking change). Xavier Morel suggested an inclusive=True flag as an alternative, saying it “might have a chance.” This proposal is that flag—keeping the default exclusive and adding an opt-in. Nick Coghlan’s arguments for half-open ranges (len formula, slicing) still apply to the default; they don’t argue against an optional parameter.

Why revisit now: The flag was dismissed in one sentence and never seriously debated. Fifteen years have passed; Python’s use in education has grown, and I’ve observed the option helping in at least one teaching case. I’m asking whether the community is open to reconsidering.

I’d welcome feedback on:

  • Whether this is worth pursuing

  • The design (keyword-only, default False)

  • Use cases where this helps or falls short

I’m new to the PEP process and would appreciate constructive feedback.

1 Like

When referring to prior discussion you should provide a link to that discussion so others can see what you are referring to.

I do not see the need for this. In fact, I see this as potentially adding more confusion and leading to even more off by one errors. While I agree that this would not be a breaking change, I don’t see any added value from it.

I will also reiterate what @oscarbenjamin said and ask to provide the link to the previous discussions so that they can be included properly in this.

Yes they are, but they also compose very unintuitively.

Inclusive ranges aren’t “more explicit”. They’re just different. It’s a choice of convention, and the best thing to do is to pick one convention and stick with it. Having options is often worse than having a consistent convention everywhere. For example, when you slice a string in Python, s[start:stop] will give you the letters from start to stop, inclusive-exclusive; since you aren’t going to see a keyword argument added to slicing strings, that would make your proposed range function inconsistent with slicing.

You’ll never avoid the need to learn how things are done. That’s part of being a programmer. Trying to kick the can down the road a little might seem tempting at first, but when it comes at the cost of design elegance, it’s not worth it.

9 Likes

Better don’t use em dashes and those slanted quote characters, makes it look like your post was written by AI and makes you look bad.

A quick scan of installed python packages on my disk has shown that about 3% of range calls add +1 to get the right-closed interval (“inclusive=True”). Not a representative sample, but IMO enough to consider this proposal.

I agree that “inclusive=True” will add confusion. What do you think about naming it itertools.closed_range()? And I would probably make both start and end mandatory.

3 Likes

thanks, I have edited the post to include the link

ACK, thank you. I did use an agent to proofread, and I did accept some suggestions. I will pay closer attention in the future.

I think that is a great idea. I didn’t want to suggest a new function as I didn’t want to clutter the global namespace with something like irange. But a function in the itertools package sounds like an elegant location.

Sorry, I didn’t mean to suggest that Inclusive ranges are “more explicit”. I meant from the perspective of my nephew, it made it explicit to him.

An inclusive range, or closed interval, is called a segment. What’s wrong with using segment() ?

def segment(start, end):
    return range(start, end + 1)

Some related prior art:

random.randint is inclusive on both ends. It’s random.randrange that acts like range.

sortedcontainers.SortedList.irange is inclusive on both ends by default.

>>> sl = SortedList('abcdefghij')
>>> it = sl.irange('c', 'f')
>>> list(it)
['c', 'd', 'e', 'f']

But it has an optional argument to change to exclusive on either or both ends. The full signature:

irange(self,
       minimum=None,
       maximum=None, 
       inclusive=(True, True), # this one
       reverse=False)

Very handy at times!

Offhand I can’t think of others. Both cases apply to values, though, not to index sequences.

Note too that reverse= on irange() doesn’t change whether the endpoints are inclusive or exclusive. Sticking a “-1” stop on a range or slice can change the set of index values produced.

I find reasoning in the presence of a negative step quite error-prone. It would, I think, be better if range(0, 5, -1) were simply the reverse of what range(0, 5) produced. That is, start and stop wholly define the index set on their own, with start < stop, and step only controlled the direction in which that set is produced. More like reverse= works for SortedList.irange().

At one point Guido appeared to agree, but it was too subtle a breaking change even for Python3 to risk making. (Of course it would apply to both range() and slicing.)

As is, the pain of dealing correctly with negative steps, in my experience, vastly exceeds the inconvenience of arranging to add 1 to stop in the relatively rare case when inclusivity is wanted. Or subtracting 1 when the step is negative - which doesn’t actually work as hoped when slicing.

seq[ : -1 : -1]

doesn’t stop at index 0: the result is actually an empty sequence. Because -1 as stop in slicing is interpreted as meaning len(seq) - 1., and when step is negative the defaujlt None for start is also interpreted as meaning len(seq) - 1.

I may have mentioned that reasoning in the presence of a negative step is error-prone :wink:

3 Likes

If the keyword approach is to be pursued, I think end_inclusive (or stop_inclusive) is a better name.
If I saw range(x, y, inclusive=False), I’d need to remind myself which end (or ends?) the keyword controls. Referring to the specific end in the keyword makes it bright as day.


If the keyword idea made it into python, would linters start to suggest always using it?
There is a precedent to that - see zip(strict=...)


Perhaps it could be worth pursuing adding dedicated x..=y/x..<y range syntax to python instead?

Odin has this syntax for end-exclusive and end-inclusive “intervals”:

for i in 0..<10 {
	fmt.println(i)
}
// or
for i in 0..=9 {
	fmt.println(i)
}

C# has end-exclusive ranges, and supports using them in what in python would be slice notation:

int[] someArray = new int[5] { 1, 2, 3, 4, 5 };
int[] subArray1 = someArray[0..2]; // { 1, 2 }
int[] subArray2 = someArray[1..^0]; // { 2, 3, 4, 5 }

Even Minecraft uses similar range syntax for certain commands, with both ends being inclusive:

execute if score @p count matches 0..10 [...]
1 Like

I suppose this is more representative: more than 100k on sourcegraph. I tried to raise max count to 1M, but it takes too much.

I hope I wrote a good regex.

Isn’t this an “obscure symmetry”? :)
Mentally, when I go reverse, I think that I must start from 5 or from len(seq). What’s the use case?

More like an obscure asymmetry. between range() and slice notation, -1 for stop in range() means -1. -1 for stop in slicing, not at all: it means len(seq) - 1 instead. Very different behavior.

Iterating an index sequence in reverse order. You never actually want to start at len(seq), because that’s an out-of-bounds index for seq.

If you have

for i in range(0, len(seq)):

and realize “oh! I need at do that in reverse order”, it’s dead easy to add a negative unit step:

for i in range(0, len(seq), -1):

except that’s not how range() works. It needs the frankly bizarre transformation to:

for i in range(len(seq)-1, -1, -1):

And then that in turn doesn’t work at all if copied to slice notation. There is no explicit integer you can use to get the same effect. In slicing you need to leave stop blank, or give None explicitly.

1 Like

I recently started doing:

range(len(seq))[::-1]

It creates 2 range objects, but most often it is noise in comparison to surrounding code.

4 Likes

I’ve used [::-1] for this as well. To me that is nice and readable although I’ve seen others saying that [::-1] itself is cryptic. There is also reversed(range(N)) which some prefer.

3 Likes

Oh, by “obscure symmetry” I meant the fact you would prefer that range(0, 5, -1) acts as range(4, -1, -1). For me, it’s a bit strange that the start would be zero and not 4.

Ok. I must admit I never had the need to do this. When do you need it?

Side note: you could write for i, x in zip(itertools.count(len(seq)-1, -1), seq) instead, but probably too verbose… ^^’

I’m one of them :smiley: It’s quite handy, but the fact you can write nothing instead of zero and None seems to me a little unpythonic. But I can’t find a good alternative.

1 Like

In frequently comes up, e.g., in dynamic programming approaches, after you realize that building up “the next row” in forward order uses “the new values” prematurely. This is why, e..g., sympy’s primepi() function does this:

        lim2 = min(lim, i * i - 1)
        for j in range(lim, lim2, -1):
            arr1[j] -= arr1[j // i] - p

For correctness, it has to change arr1 form highest index to lowest.

I didn’t search for that. I just happened to notice it this week. In context, the end conditions are exceedingly delicate, and it doesn’t help that a negative stride changes the index sequence itself, not just the order in which it’s materialized. In other parts of the same function, the index set must be traversed in forward order.

2 Likes

You can use an integer for stop. Just remember that -1 is the last item, -2 is the item before that, …, -len(seq) is the first item, and then it’s clear that -len(seq)-1 is the “item before the first item”, which is a perfect exclusive stop with a step of -1.

>>> seq = "abcde"
>>> seq[:-len(seq)-1:-1]
'edcba'

Or did you mean something else?

This looks unfamiliar only because slicing has the “add the sequence length to negative indices” magic, and ranges don’t (they can’t, of course). And because negative slices don’t change that behaviour. And probably a lot of other reasons, I remember a lot of discussion about this.

2 Likes