New submission from WloHu :
###
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
<https://bugs.p