Interpret "for i in n" as "for i in range(n)" if n is an int

I expect that this idea will be shot down :slight_smile: but I’m curious to hear why.

As a pure convenience / syntactic sugar thing, I’d love not having to write “for i in range(n)” ever again and instead be able to say “for i in n” with no ambiguity (at least when n is an integer at runtime). Is there any chance that this could be proposed for discussion or is it, for some reason, totally out of the question?

1 Like

Well, it’s not one that’s completely new to us :slight_smile:

There are a few things that go with the concept of “being a collection”. A collection contains things, and when you iterate over it, you get handed each of those things in turn; you can also take an existing thing and ask “is this in the collection?”. Example:

numbers = range(10, 50, 3)
for n in numbers:
    print(n, n in numbers)

This will print “True” for each value. Now, you can’t do this trick with everything (some objects destroy themselves as you iterate over them, so you can EITHER ask if something’s in the collection OR iterate, but not both), but for most common sequences, it’s the case.

So if for i in 10: should yield the series of integers from 0 to 9 inclusive, then 3 in 10 should be True. That doesn’t really make sense. There are a few logical ways that one integer could be “contained in” another (treating the containing integer as a set of prime factors, or powers of two, or maybe non-consecutive Fibonacci numbers), but I don’t think many people would agree that “is non-negative and smaller than it” is one of those ways.

That’s really the problem here: it’s kinda neat to be able to iterate N times conveniently, but to do so, you would have to forfeit tidiness somewhere else.

2 Likes

Those (very common in Mathematics) who see natural numbers as von Neumann ordinals will see 3 in 10 as True. Not saying that I think that Python should do this, though.

6 Likes

That sounds like for _ in instead of their for i in and wouldn’t even need a range. I’ve sometimes desired that, like an n times: ... syntax similar to Ruby’s n.times { ... }. Would be faster since unlike the range iterator, it wouldn’t have to produce int objects.

2 Likes

That’s true, and if ALL you need is “do this N times”, without any kind of loop counter, then sure! A very narrow use-case, but I know a few languages that have dedicated syntax for this. To have an iteration counter though, you would need to yield consecutive integers in some way.

I would say that interfaces should be as small as possible while remaining as useful as possible. Integers don’t implement iterable because they are extremely useful without that interface. Yes, you could add the iterable interface, but doing that would obscure bugs that would otherwise be caught in exchange for the minor convenience you propose.

Also, your proposal isn’t that much of a convenience since, in my opinion, a language should be optimized for reading rather than writing.

7 Likes

Furthermore, it’s not entirely clear what should be iterated over. You could iterate over the bits (or digits in any base really), the 30-bit units ints are made of, or the range 0-n. None is obvious so it’s probably for the best that you have to have to choose one explicitly.

2 Likes

Appreciate everyone’s thoughts here.

Perhaps I do an above average amount of “for i in range(n)” for some reason, and thus would disproportionately benefit from the convenience :slight_smile:

Maybe the cleanest solution (though unlikely to exceed the bar requied to introduce a new “primitive”) would then be an upto keyword to be used like: “for i upto n” which would behave as “for i in range(n)” if n is an integer, and raise an error otherwise.

What do you do that requires so much for i in range(n)?
I use it sparingly because there’s almost always a better alternative:
Iterating over a container/sequence/iterator? Just iterate over it directly:

container = ["a", "b", "c"]
for item in container:
    print(item)
    # a
    # b
    # c

Need the index as well as the objects? Use the enumerate builtin:

for i, item in enumerate(container):
    print(f"{i}, {item}")
    # 0, a
    # 1, b
    # 2, c

Need to iterate over two (or more) things at the same time? Use zip:

other_container = [3.14, 2.71, 42.0]
for item, other_item in zip(container, other_container):
    print(f"{item}, {other_item}")
    # a, 3.14
    # b, 2.71
    # c, 42.0

Need to iterate over a thing in reverse? Use reversed:

for item in reversed(container):
    print(item)
    # c
    # b
    # a

Think you have a special use case that isn’t covered by builtins? Look at the itertools module (It’s one of the most useful ones in the stdlib).

Typically, you only need to iterate using indices when you mutate the underlying container, otherwise you should avoid it.

4 Likes

Just for fun: we already have syntax for that! It’s spelled for[]in[[]]*. No int objects produced, and no dummy variable names needed. Example usage:

>>> for[]in[[]]* 5: print("Hello")
... 
Hello
Hello
Hello
Hello
Hello

The variant syntax for()in((),)* is one character longer, but likely more efficient (especially for a constant number of repetitions):

>>> dis.dis("for()in((),)* 3: print('Hello')")
  0           0 RESUME                   0

  1           2 LOAD_CONST               0 (((), (), ()))
              4 GET_ITER
        >>    6 FOR_ITER                11 (to 32)
             10 UNPACK_SEQUENCE          0
             14 PUSH_NULL
             16 LOAD_NAME                0 (print)
             18 LOAD_CONST               1 ('Hello')
             20 CALL                     1
             28 POP_TOP
             30 JUMP_BACKWARD           13 (to 6)
        >>   32 END_FOR
             34 RETURN_CONST             2 (None)

I’ll freely admit it’s not the most readable syntax …

19 Likes

It’s quite possible that I use it in cases where there exist better alternatives. However, I just searched for cases where this pattern is used in the standard lib of a Python 3.12 env I had handy. It’s not a million hits, but it’s still reasonably frequent.

Seeing a bunch of “for i in range(len(x))” which I find even more jarring :slight_smile: and also use quite a bit.

$ grep -r --include="*.py" "for .* in range([^,]*):" /home/andrei/.pyenv/versions/3.12.1/lib/python3.12/ | wc -l
1377

$ grep -r --include="*.py" "for .* in range([^,]*):" /home/andrei/.pyenv/versions/3.12.1/lib/python3.12/ | head -n 40
/home/andrei/.pyenv/versions/3.12.1/lib/python3.12/sunau.py:    for i in range(4):
/home/andrei/.pyenv/versions/3.12.1/lib/python3.12/sunau.py:    for i in range(4):
/home/andrei/.pyenv/versions/3.12.1/lib/python3.12/warnings.py:            for x in range(stacklevel-1):
/home/andrei/.pyenv/versions/3.12.1/lib/python3.12/gzip.py:            for i in range(count // self._buffer_size):
/home/andrei/.pyenv/versions/3.12.1/lib/python3.12/turtle.py:            for i in range(self.bufsize):
/home/andrei/.pyenv/versions/3.12.1/lib/python3.12/turtle.py:        >>> for _ in range(36):
/home/andrei/.pyenv/versions/3.12.1/lib/python3.12/turtle.py:        >>> for i in range(200):
/home/andrei/.pyenv/versions/3.12.1/lib/python3.12/turtle.py:        for i in range(steps):
/home/andrei/.pyenv/versions/3.12.1/lib/python3.12/turtle.py:        >>> for i in range(200):
/home/andrei/.pyenv/versions/3.12.1/lib/python3.12/turtle.py:        >>> for i in range(8):
/home/andrei/.pyenv/versions/3.12.1/lib/python3.12/turtle.py:            for _ in range(steps):
/home/andrei/.pyenv/versions/3.12.1/lib/python3.12/turtle.py:        >>> for i in range(4):
/home/andrei/.pyenv/versions/3.12.1/lib/python3.12/turtle.py:        >>> for i in range(8):
/home/andrei/.pyenv/versions/3.12.1/lib/python3.12/turtle.py:        for i in range(3):
/home/andrei/.pyenv/versions/3.12.1/lib/python3.12/turtle.py:            for _ in range(4):
/home/andrei/.pyenv/versions/3.12.1/lib/python3.12/turtle.py:        for i in range(5):
/home/andrei/.pyenv/versions/3.12.1/lib/python3.12/turtle.py:        for i in range(5):
/home/andrei/.pyenv/versions/3.12.1/lib/python3.12/turtle.py:        for _ in range(18):
/home/andrei/.pyenv/versions/3.12.1/lib/python3.12/turtle.py:            for _ in range(3):
/home/andrei/.pyenv/versions/3.12.1/lib/python3.12/turtle.py:        for _ in range(4):
/home/andrei/.pyenv/versions/3.12.1/lib/python3.12/email/_header_value_parser.py:    for pos in range(len(fragment)):
/home/andrei/.pyenv/versions/3.12.1/lib/python3.12/idlelib/pyshell.py:        for i in range(3):
/home/andrei/.pyenv/versions/3.12.1/lib/python3.12/idlelib/pyshell.py:    for i in range(len(sys.path)):
/home/andrei/.pyenv/versions/3.12.1/lib/python3.12/idlelib/idle_test/test_configdialog.py:        for _ in range(2):
/home/andrei/.pyenv/versions/3.12.1/lib/python3.12/idlelib/idle_test/test_sidebar.py:            for i in range(steps):
/home/andrei/.pyenv/versions/3.12.1/lib/python3.12/idlelib/idle_test/test_undo.py:        for i in range(max_undo + 10):
/home/andrei/.pyenv/versions/3.12.1/lib/python3.12/idlelib/pyparse.py:        for tries in range(5):
/home/andrei/.pyenv/versions/3.12.1/lib/python3.12/idlelib/zzdummy.py:        for pos in range(len(lines) - 1):
/home/andrei/.pyenv/versions/3.12.1/lib/python3.12/idlelib/run.py:    for i in range(3):
/home/andrei/.pyenv/versions/3.12.1/lib/python3.12/idlelib/run.py:    for i in range(len(tb)):
/home/andrei/.pyenv/versions/3.12.1/lib/python3.12/idlelib/scrolledlist.py:    for i in range(30):
/home/andrei/.pyenv/versions/3.12.1/lib/python3.12/idlelib/editor.py:            for insertpt in range(len(line)):
/home/andrei/.pyenv/versions/3.12.1/lib/python3.12/idlelib/editor.py:    for i in range(20):
/home/andrei/.pyenv/versions/3.12.1/lib/python3.12/idlelib/debugger.py:        for i in range(len(stack)):
/home/andrei/.pyenv/versions/3.12.1/lib/python3.12/idlelib/format.py:        for pos in range(len(lines)):
/home/andrei/.pyenv/versions/3.12.1/lib/python3.12/idlelib/format.py:        for pos in range(len(lines)):
/home/andrei/.pyenv/versions/3.12.1/lib/python3.12/idlelib/format.py:        for pos in range(len(lines) - 1):
/home/andrei/.pyenv/versions/3.12.1/lib/python3.12/idlelib/format.py:        for pos in range(len(lines)):
/home/andrei/.pyenv/versions/3.12.1/lib/python3.12/idlelib/format.py:        for pos in range(len(lines)):
/home/andrei/.pyenv/versions/3.12.1/lib/python3.12/idlelib/format.py:        for pos in range(len(lines)):

Oh yeah you said for i in range(n) and not for i in range(len(x)). I think I agree that the first case is useful at times. But not so useful and common that it needs special syntax, beyond that it’s kinda (if you squint really hard) syntactic sugar for

i = 0
while i < n:
    print(i)
    i += 1

If I saw someone write for[]in[[]]* I would call the police :joy:

11 Likes

Hey, it could be worse. Have you seen the “wide addition” operator?

x = 5
x -=- 3
print(x) # 8
16 Likes

We really do a need a FAITHBR (Frequently Asked Ideas that Have Been Rejected) somewhere …

But anyway, I’m not supportive of this idea, but I don’t think this logic holds:

Back in the day, Python was all about Sequences – or maybe Collections. But modern Python is more about Iterables.

for _ in is a way to iterate, that requires an iterable.

in by itself invokes __contains__ – and is about inclusion in a collection.

They use the same word, and there is often a parallel, but that doesn’t mean that there has to be – there are any number of iterables that are not containers. There is nothing surprising about that.

In fact, I wonder if the range object would be a container if it hadn’t evolved from the original range() that created a realized list. Does it get used that way often? (not in my code).

I still don’t think it’s a good idea for the other reasons posted in this thread, but the idea that anything iterable should be container is not compelling to me.

NOTE: If an object is both an iterable and a container, then yes, there should be symmetry – e.g. what dicts do: using the keys for iteration and contains.

4 Likes

But the current Python spec disagrees. The in operator falls back to iteration if no __contains__ method is implemented, I would think that is what @Rosuav was talking about:

class TestIter:
    def __iter__(self):
        yield 1
        yield 2
        yield 3

print(2 in TestIter())

prints True

2 Likes

Ouch! I had forgotten about that – that’s got to be a legacy of the old Sequence-forcused model – but it’s not a great idea today. It’s likely to be highly inefficient, and perhaps destructive if a user inadvertently exhausts an iterator [*]. In fact, I’m pretty sure that there have been discussions about deprecatiing that behavior. In any case, I would likely define a __contains__ that raises for iterables that are not containers.

[*] or even worse, a endless loop if the iterable never terminates – e.g.:

'fred' in itertools.count()

locks my interpreter – CTRL+C doesn’t even work, presumably, becasue count() is written in C?

Note to self – propose a PR where __contains__ is defined, and raises, for itertools.count()

3 Likes

Well don’t use it for bad stuff, only for good stuff.

1 Like

Yes, but it’s not JUST a fallback/default, it’s also an excellent elegance of design. If you go through a collection and look at everything in that collection, each of those things is in the collection. It stands to reason.

1 Like

:person_shrugging: Not sure tbh. I don’t think it should be changed now, but I don’t think it would have been added in a world post-generators. You can have lots of objects that have __iter__ but which don’t really represent a collection of things, and people might not expect it to suddenly exhaust a generator. I personally never have been bitten by this, but I also don’t think I ever benefited from it. IIRC, the collections.abc.Collection interface implements __contains__ by explicitly iterating instead of relying on the implicit nature, so in a world where those ABCs exists, I don’t see much a point in this behavior.

But this is pretty off-topic now. If anyone wants to actually suggest removing this behavior or overwriting __contains__ for a few builtin iters, they can suggest that in a different thread.

Either of these option should probably be done before the OP proposal can be implemented [1] so that there is an established way to make sure that 1 in 3 raises a clear exception.


  1. not that I am in favor of it. IMO saving the effort to type range() is not worth it ↩︎

1 Like