# 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 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

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

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
20 CALL                     1
28 POP_TOP
30 JUMP_BACKWARD           13 (to 6)
>>   32 END_FOR
34 RETURN_CONST             2 (None)
``````

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 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/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

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 `Sequence`s – or maybe `Collection`s. 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

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