"for i in range()" to do an infinite loop with a counter

Hi,
Usually in Python we can avoid the i = 0i += 1 paradigm that we use in other languages when we need to count things, thanks to enumerate(...), for i in range(100), etc.
Along the years I have nearly always found a more “pythonic” replacement for code containing i = 0i += 1.

There is an exception with this code: an infinite loop with a counter:

i = 0
while True:
    ...
    if breaking_condition:
        break
    i += 1

Proposal: could we accept that range() without any parameter gives an infinite iteration?

Example:

for i in range():  # infinite loop
    print(i)
    if breaking_condition:
        break

Basically range() without parameter would have the same behaviour than:

import itertools
for i in itertools.count():
    if breaking_condition:
        break

Have you considered making this available directly, without any import, with range()?


Linked topic: Looping from 1 to infinity in Python.

2 Likes

Since the itertools.count() version works fine, I don’t think we’d add another way of doing the same thing.

7 Likes

Maybe, but this would be nice generalization of
for i in range(100) (count from 0 to 99) to for i in range() (count from 0 to infinity).

This benefit comes for free, since range currently can’t be called with 0 argument.

Also, it seeems rather logical and nice syntax:

range(stop)
range(start, stop)
range(start, stop, step)
range()            # we don't have a "stop" argument so it never stops
2 Likes

And since we don’t have a “start” argument, it never starts, right? :stuck_out_tongue_winking_eye:

Jokes aside, you would need to define len(range()), reversed(range()), range()[-1], range()[::-1], x in range(). And you would see that range() is a very special object, different from ordinary range object in many ways. So why call it range() and not count()? I am sure that this option was already discussed when the itertools module was added, and there were reasons of adding itertools.count().

9 Likes

Not having a start value would be a non-starter (pun not intended). It would have fewer features than itertools.count(). I can see maybe doing something that has more features, but not the same or fewer.

We could have a start value and no stop value:

for i in range(start=17):   # no stop argument
   print("We'll never stop!")
1 Like

I think range() is already complicated enough.

2 Likes

Probably.

It was just an idea because I often see people doing i = 0; while True: ... i += 1 in code, and there seemed to be an improvement “for free” with a range with no stop: range() or range(start=100).
But anyway, not a big deal.

Have a nice day everyone!

Thanks for starting this thread. It just so happens that yesterday I wrote a i = 0; while True: ... i += 1 loop, and today I changed it to a for i in itertools.count() loop. Definitely an improvement, and one I wouldn’t have made without this thread.

4 Likes

Haha, happy for this :slight_smile:
This is a good sign that we shouldn’t have to import anything to do this (in the same way we don’t import anything to enumerate), and that for i in range() would be perfect, but I promiss I stop now haha! (we can close this thread).

If you really don’t want to import itertools, you could approximate ‘forever’ by ‘Googol’

for i in range(10**100):
    print(i)
    if i > 3:
        break

Actual ‘forever’ can be attained by the longer and more complex:

for i, _ in enumerate(iter(lambda: 'hell', 'freezes over')):
    print(i)
    if i > 3:
        break

Both offer the option of a start value, and the first one with range also facilitates step (if you want to reach googol faster).

7 Likes

Serhiy’s comment here is the key conceptual reason unbounded ranges aren’t allowed: they’re defined as memory efficient computed sequences rather than consumable iterators, so they’re expected to have a lower and upper bound, even though those limits might be large.

That said, there are already some range operations that throw an exception if the sequence length doesn’t fit in 64 bits, so it isn’t unthinkable to have a special case for “stop=None”. However, unlike the “computed sequence that wouldn’t fit in a 64 bit address space” case, the unbounded sequence case would either slow down multiple operations with extra checks for None, or else we’d have to play constructor games to return a different object type for the special case (and such games are inevitably confusing to maintain).

By contrast, “from itertools import count” is simple to understand conceptually and straightforward to maintain.

1 Like

for len(range()) and range()[-1] just return float('inf').

1 Like

That would require changes to len, which currently doesn’t allow float values. For example:

>>> class Foo:
...   def __len__(self):
...     return float('inf')
... 
>>> len(Foo())
Traceback (most recent call last):
  File "<stdin>", line 1, in <module>
TypeError: 'float' object cannot be interpreted as an integer

My understanding is essentially that len does some sanity checks on the result of __len__.

Sure, it /could/ be changed so that floating-point (or maybe just infinite) lengths are acceptable, but considering the amount of code that assumes that len will return an integer, that seems a like a non-starter to me.

As far as the range()[-1], maybe that would be acceptable (though it has a similar problem in that every element in a range is supposed to be an integer), but that’s hardly the only issue. As mentioned, what about reversed(range())? Or, even more simply, how about range()[-2]? Should /every/ negative index just return float('inf')?

Having an object that represents a range of integers without an upper bound that still implements parts of the sequence protocol (unlike itertools.count which is really just an iterator) might be useful in some situations, but I think that it should be its own class, rather than complicating range to support it.

This was already answered definitively 11 months ago, no need to resurrect it now.