[New-bugs-announce] [issue36066] Add `empty` block to `for` and `while` loops.

WloHu report at bugs.python.org
Thu Feb 21 09:29:45 EST 2019


New submission from WloHu <wlohuu at gmail.com>:

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

----------
components: Interpreter Core
messages: 336221
nosy: wlohu
priority: normal
severity: normal
status: open
title: Add `empty` block to `for` and `while` loops.
type: enhancement
versions: Python 3.7

_______________________________________
Python tracker <report at bugs.python.org>
<https://bugs.python.org/issue36066>
_______________________________________


More information about the New-bugs-announce mailing list