Loop with else clause
Further to our discussion of how to improve a code review's discovery of
the mistaken handling of a for...else... construct:-
Yesterday was a national holiday, but today gave some opportunity to
research. Way back in 2009 there was spirited discussion over on the
Python Ideas list (warning, even the mailing list's index covers
- this confusion is not new by any measure, herewith a list of previous
occasions "fists were raised concerning for..else."
- an excellent summary of the 2009 debate which offers no less than six
ways to 'improve' for... else...
- (as mentioned earlier) the BDFL weighed-in a couple of times. His
regret is: "That's a flaw, and I don't quite know what to do about it.
It's about 20 years too late to remove or rename it. But we probably
should not do more of these. That's a lesson."
(OK, so make that thirty years - older than the coder who the
code-review noticed falling into this coding 'gotcha'!)
- herewith a (rather complicated) suggestion, and critique
- one rather hopeful option (actual words to be used notwithstanding)
for i in SEQ:
Somewhat related, PEP 548 proposed an "More Flexible Loop Control". This
was addressing the confusion caused by break within a loop. It was rejected.
Each of the above addresses issues 'within', that is to say happenings
during iteration - whether the entire loop or iteration cut-short by a
break (and thus the idea that "else" might be re-worded to indicate
'after a break').
However, as mentioned by one contributor, the specific use-case our team
faced was an issue that arises prior to the loop. Alternately-expressed:
that according to Python's logic, prevents even a single iteration of
that loop. Thus, any 'solution' would reside outside of for and while
statements because they only consider if a loop should continue or
terminate - not handling the question of whether it should start at all!
PEP 315 is the only discussion (I've found) which looks 'outside' or
'before' the loop itself. It proposed an "Enhanced While Loop",
attempting to separate 'setup' or loop control from loop content. It was
So, reading-around brought nothing much useful. Back to the code-face...
Thank you to the several folk who responded with ideas to express/improve:
process_list() #the heading and for-loop, as above
NB this is a constructed 'toy example' attempting to be the shortest
illustration of use-cases and invented purely to communicate the need
and structure. It was expected to be interpreted as pseudo-python-code.
(you'd not use/allow "process_list" as the name of a function, would you?)
(With apologies as necessary) one of the dangers of 'toy examples' is
the reader taking them at face value, instead of as (over-)simplified
illustrations. In 'real life' the loop code and the no-loop exception
are both considerably longer than a single line. Accordingly, using a
function would be a good way to summarise and self-document the
activity, ie the if statement's two code-blocks would make the whole
statement very/too-long (readability)!
The "if list:" expression is overly-simplistic. The recommendation of
"if len(list):" is absolutely sound, for reasons of polymorphism.
In-lieu of a Python construct, there are definitely situations when use
of a sentinel makes better sense. However, given their risk, in many
ways Python tries to avoid using such, eg consuming iterators until a
StopIteration exception is returned. (includes files, is subsumed by
ContextManagers...), thus "pythonic". That said, the classic use of
for... else... is in searching for a particular element within an
iterable which has all the hallmarks of a "sentinel".
Today, kicking ideas around, I coded three other possible 'solutions'
for the review team's discussions:
One of these involves coding three functions: the decision (yielding a
boolean), the expected-case, and the unusual-case. The satisfaction in
this was readability factors with a simple if statement..
Somewhat more complex, and I feel a bit OTT, was to sub-class list() and
write a method which would indicate an empty list, plus include much of
both the expected-case and the empty-list methods therein. Somehow
"class ListShouldNotBeEmpty" doesn't seem a catchy (if descriptive)
title - however, it works! The decision 'function' could also/then be
made @property and (perhaps) thus contribute to readability.
Lastly, I 'remembered' conditional expressions. Thus, furthering the
idea of two/three functions:
process_list() if list_has_contents() \
Functional = yes.
Pythonic = yes.
Readable = much debated/relates to the expertise of the coder/reader