Allow `range(start, None, step)` for an endless range

Currently, to iterate over finite arithmetic sequences of integers, range is used, as in:

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

For an infinite arithmetic sequence, there are a few approaches. One can replace the ‘10’ with a long sequence of nines, write a generator function or just switch to a while loop, but the current ‘best’ way is using itertools:

import itertools
for i in itertools.count():
    print(i)

There are a few downsides to this. Firstly, itertools.count is doing a very similar thing to range, yet seems on first glance completely unrelated. Without knowing, it’s easy to assume that itertools.count does a similar thing to list.count or str.count, rather than range.

In some other languages the finite and infinite cases look very similar, C (and other languages with the same loop syntax) and Rust being two examples.

Secondly, infinite loops like this are quite common in tiny example programs, e.g.

import itertools
primes = []
for i in itertools.count(2):
    if all(i % p for p in primes):
        primes.append(i)
        print(i)

In these cases, having to import itertools to get an infinite loop makes the program significantly longer.

I propose using range(start, None, step) for an endless range. This makes sense, as the ‘stop’ parameter is None, i.e. there is no ‘stop’ and the iterable goes on forever. The step and start parameters could be omitted as normal.

I would also suggest range() should default to an end of None with zero arguments:

for i in range():
    print(i)

Downsides:

  • Performance hit (probably tiny) on finite loops
  • len of a range is no longer always nice
  • More ways to do the same thing (perhaps deprecate itertools.count?)

I would not want range(None) to be valid. There’s too much of a chance for an error to creep in. range(some_function()). What if some_function accidentally returned None? Now it’s an infinite loop.

Otherwise I’m -0. I don’t think itertools.count is too hard to find.

2 Likes

Yes, that makes sense. I’ve just managed to find this discussion, which didn’t really seem to go anywhere. The suggestion there was using ... (i.e. Ellipsis) instead of None, which might be better.

Just use a while loop with an explicit counter.

i = 0
while True:
    do_whatever()
    i += 1

I know it sounds odd to worry about performance of an infinite loop (“my
code is so fast it can run an infinite loop in 20 microseconds!”) but
there are cases where you exit out of the loop and performance matters.

In those cases, itertools.count is faster and more efficient, and
cleaner too, as you don’t have to worry about manually updating the
counter.

The fact that itertools.count is marginally faster than explicitly incrementing a counter is a CPython implementation detail due to itertools being implemented as a c-extension module. I don’t know if that is true when the code is transpiled for cython, numba or pythran. I am pretty sure in PyPy it will be slower, since in general itertools is slower on PyPy than an explicit implementation due to the extra indirection.

2 Likes

I really like your proposal.
I know about itertools.count, but find it confusing and quite cumbersome to import just for that.
I don’t think there are any real disadvantages:

  • performance. Very unlikely to be significant
  • implementation of len. Just a NotImplemented exception could be raised
  • duplicate functionality There are so many cases of that in Python, that it can’t be a serious problem. Please don’t deprecate itertools.count

“Endless range” is an oxymoron (by definition a range has endpoints) while itertools.count does exactly what it sounds like. You also won’t find much support for deprecating something in itertools that already works well and is widely used.

If this is for teaching purposes, I think you already landed on the right solution. The upper end of the range should be some really large number, perhaps 10**20 or a named constant like sys.maxsize. The practical difference between running forever and running for several millennia doesn’t seem like a big deal. Plus, you can use this as a motivating example for students to learn generators.

1 Like

Mark said:

“Endless range” is an oxymoron (by definition a range has endpoints)

I think mathematicians will disagree.

Both the codomain and image can be infinite in either direction, or
both.

So does Ruby:

https://ruby-doc.org/core-2.6/Range.html#class-Range-label-Endless+Ranges

And of course, in common English parlance, we can talk about ice cream
that comes in an endless range of flavours, shops that stock an endless
range of parts, and holiday parks that offer an endless range of fun
activities. And of course, there is the iconic American image of the
lone cowboy riding riding across the endless range.

Some downsides of itertools.count:

  • Its not a sequence like range;
  • and membership testing consumes values.

On the other hand, it does support arbitrary step sizes.

And of course, the problem with the original proposal that range(start, None, step) should give an endless range, is precisely that this would mean that range objects could no longer be sequences either (from collections.abc, objects that implement Sequence must be sized and reversible, neither of which can be true for infinite ranges).

Whether this is important in practice, is an open question, of course, but note that the following works with range objects at the moment:

>>> range(10)[2:-1]
range(2, 9)
>>> range(10)[2:-1:-1]
range(2, 9, -1)

With infinite ranges, that’s no longer possible. And before anyone suggests it, I really don’t like the idea of range objects that might support Sequence operations, depending on the arguments used to create them…

It’s not that hard to use itertools.count, or write your own infinite generator, so I don’t think the trade-offs for this proposal are worth it.

8 Likes

Of course words have multiple meanings, but the specific meaning of range() in Python is clearly not referring to the codomain of a mathematical function. The dictionary offers the following:

a sequence, series, or scale between limits

You might argue that “limit” does not necessarily imply finite limits, and since I don’t think further debate about the semantics of the English language is productive, I won’t belabor the point.

I am a bit surprised that range(2, math.inf) does not work, but that seems like a more promising approach for infinite ranges than overloading None to mean “no upper limit”.