This issue tracker has been migrated to GitHub, and is currently read-only.
For more information, see the GitHub FAQs in the Python's Developer Guide.

classification
Title: Add `empty` block to `for` and `while` loops.
Type: enhancement Stage: resolved
Components: Interpreter Core Versions: Python 3.7
process
Status: closed Resolution: postponed
Dependencies: Superseder:
Assigned To: Nosy List: matrixise, terry.reedy, wlohu, xtreak
Priority: normal Keywords:

Created on 2019-02-21 14:29 by wlohu, last changed 2022-04-11 14:59 by admin. This issue is now closed.

Messages (7)
msg336221 - (view) Author: WloHu (wlohu) Date: 2019-02-21 14:29
###
Description

Adding `empty` block to loops will extend them to form for-empty-else and while-empty-else. The idea is that `empty` block will execute when loop iteration wasn't performed because iterated element was empty. The idea is taken from Django framework' `{% empty %}` block (https://docs.djangoproject.com/en/2.1/ref/templates/builtins/#for-empty).

###
Details

There are combinations how this loop should work together with `else` block (`for` loop taken as example):
1. for-empty - `empty` block runs when iteration wasn't performed, i.e. ended naturally because of empty iterator;
2. for-else - `else` block runs when iteration ended naturally either because iterator was empty or exhausted, behavior the same as currently implemented;
3. for-empty-else - in this form there is split depending on the way in which loop ended naturally:
    -- empty iterator - only `empty` block is executed,
    -- non-empty iterator - only `else` block is executed.

In 3rd case `else` block is not executed together with `empty` block because this can be done by using for-else form. The only reason to make this case work differently is code duplication in case when regardless of the way, in which loop ended naturally, there is common code we want to execute. E.g.:
```
for:
    ...
empty:
    statement1
    statement2
else:
    statement1
    statement3
```

However implementing the "common-avoid-duplication" case will be inconsisted with `try-except` which executes only 1st matching `except` block.

###
Current alternative solutions

In case when iterable object works well with "empty test" (e.g.: `list`, `set`) the most simple solution is:
```
if iterable:
    print("Empty")
else:
    for item in iterable:
        print(item)
    else:
        print("Ended naturally - non-empty.")
```

Which looks good and is simple enough to avoid extending the language. However in general this would fail if `iterable` object is a generator which is always truthy and fails the expectations of "empty test".
In such case special handling should be made to make it work in general. So far I see 3 options:
- use helper variable `x = list(iterable)` and do "empty test" as shown above - this isn't an option for unbound `iterable` like stream or asynchronous message queue;
- test generator for emptiness a.k.a. peek next element:
```
try:
    first = next(iterable)
except StopIteration:
    print("Empty")
else:
    for item in itertools.chain([first], iterable):
        print(item)
    else:
        print("Ended naturally - non-empty.")
```

- add `empty` flag inside loop:
```
empty = True
for item in iterable:
    empty = False  # Sadly executed for each `item`.
    print(item)
else:
    if empty:
        print("Empty")
    else
        print("Ended naturally - non-empty.")
```

The two latter options aren't really idiomatic compared to proposed:
```
for item in iterable:
    print(item)
empty:
    print("Empty")
else:
    print("Ended naturally - non-empty.")
```

###
Enchancement pros and cons
Pros:
- more idiomatic solution to handle natural loop exhaustion for empty iterator,
- shorter horizontal indentation compared to current alternatives,
- quite consistent flow control splitting compared to `try-except`,
- not so exotic as it's already implemented in Django (`{% empty %}`) and Jinja2 (`{% else %}`).

Cons:
- new keyword/token,
- applies to even smaller number of usecases than for-else which is still considered exotic.

###
Actual (my) usecase (shortened):
```
empty = True
for message in messages:
    empty = False
    try:
        decoded = message.decode()
    except ...:
        ...
    ... # Handle different exception types.
    else:
        log.info("Success")
        break
else:
    if empty:
        error_message = "No messages."
    else:
        error_message = "Failed to decode available messages."
    log.error(error_message)
```

###
One more thing to convince readers

Considering that Python "went exotic" with for-else and while-else to solve `if not_found: print('Not found.')` case, adding `empty` seems like next inductive step in controling flow of loops.

###
Alternative solution

Enhance generators to work in "empty test" which peeks for next element behind the scenes. This will additionally solve annoying issue for testing empty generators, which currently must be handled as special case of iterable object. Moreover this solution doesn't require new language keywords.
msg336223 - (view) Author: Karthikeyan Singaravelan (xtreak) * (Python committer) Date: 2019-02-21 14:41
I would recommend posting this on python-ideas to get some feedback. This introduces new control flow and breaks some old assumptions as in third case empty block is executed and there might be code that depends upon current for-else behavior where else should be executed. Also reading the examples initially this seems to add little cognitive overhead too since there is now empty and else that are executed based on empty iterable, breaking out of the loop, natural ending of the loop which might make this little hard to teach too.
msg336224 - (view) Author: Stéphane Wirtel (matrixise) * (Python committer) Date: 2019-02-21 14:42
This issue should be discussed on python-ideas

https://mail.python.org/mailman/listinfo/python-ideas
msg336225 - (view) Author: Karthikeyan Singaravelan (xtreak) * (Python committer) Date: 2019-02-21 14:46
Slightly similar proposal in the past : https://mail.python.org/pipermail/python-ideas/2016-March/038897.html
msg336226 - (view) Author: Stéphane Wirtel (matrixise) * (Python committer) Date: 2019-02-21 14:51
yep and there was no followup. maybe we could close this issue.
msg336341 - (view) Author: WloHu (wlohu) Date: 2019-02-22 18:24
Sorry for beeing ignorant about python-ideas. The linked proposal is almost exatly what I've requested here, the differences are that it discusses other keyword alternatives and mine suggests an alternative for making empty generators false.

If lack of follow-up in the linked proposal can be considered as "won't do" then this issue can be closed. Should I repost any part of this to python-ideas or whole case is closed?
msg336890 - (view) Author: Terry J. Reedy (terry.reedy) * (Python committer) Date: 2019-03-01 04:29
Syntax changes need PEPs.  By default, syntax changes are rejected.  This is especially true for new keywords, as any acceptible word will already be in use, and hence its introduction *will* break code.

We are still having issues over having temporarily made 'async' and 'await' 'semi-keywords' (for 2 versions) to smooth out breakage.  So new keywords must enable something previously difficult.  But implementing the 'empty' idea is fairly trivial, as demonstrated in the initial post.

A third alternative is a peak(iterator) class with a __bool__ method.  I posted one once, probably on python-list.  But the extra overhead every iteration must be larger than the simple assignment, which is so fast one should not worry about it.  I suspect the flag also beats the chain alternative.
---

[More opinionated than the above ...]

Calling what is easy and works today 'non-idiomatic' is nearly pure opinion and not persuasive as a rhetorical device.

The complicated rules for else-never interaction to be far more 'anti-idiomatic' from a comprehension point of view.  In any case, a flag allows easy, flexible, and precise ordering of never, something, and either-way code.
History
Date User Action Args
2022-04-11 14:59:11adminsetgithub: 80247
2019-03-01 04:29:31terry.reedysetstatus: open -> closed

nosy: + terry.reedy
messages: + msg336890

resolution: postponed
stage: resolved
2019-02-22 18:24:52wlohusetmessages: + msg336341
2019-02-21 14:51:25matrixisesetmessages: + msg336226
2019-02-21 14:46:57xtreaksetmessages: + msg336225
2019-02-21 14:42:43matrixisesetnosy: + matrixise
messages: + msg336224
2019-02-21 14:41:42xtreaksetnosy: + xtreak
messages: + msg336223
2019-02-21 14:29:45wlohucreate