Tuesday 18 October 2011 — This is 13 years old. Be careful.
Python has an unusual appendix to its looping structures. A “for” or “while” loop can also have an “else” clause. It was the topic of this exchange on Twitter:
@raymondh: else-clauses in #python’s for/while loops are trivially easy to explain but some minds just rebel at the thought: http://bit.ly/for_else
@holdenweb: @raymondh I think we can all agree that Guido may have taken his dislike of adding keywords a step too far with for/while ... else. #python
@raymondh: @holdenweb In 1991, “else” was the obvious choice because of the traditional way compilers implemented while-loops: pastebin.com/tY35CTJ4
If something is trivial to explain, but people aren’t getting it, maybe we should put more work into explaining it. Steve Holden knows a lot about Python, and feels that “else” was a stretch for this purpose, so even he doesn’t seem comfortable with for/else. It took me a while to get it too, but now it makes complete sense to me. Let me explain.
The classic (trivial) description of for/else is this: the “else” clause is executed if the loop completes normally, but not if it was interrupted with a “break”. This explanation is technically correct, but is also confusing. It seems like “else” should instead be “and also”, since the else clause isn’t an alternative to the loop, it’s added on, but only if the entire loop completed. So it seems like the wrong word. And Raymond’s resort to an examination of compiled code doesn’t help anyone understand it. One of the great things about Python is Guido’s insistence on usability of the language as a guiding principle in its design. There should be a user-centric explanation that makes sense.
Consider a common structure of a “for” loop:
for thing in container:
if something_about(thing):
# Found it!
do_something(thing)
break
else:
# Didn't find it..
no_such_thing()
Here you can see something interesting: there’s an “if”, and there’s an “else”. Those two words often go together, but not the way they are used here. But the pairing is important, and is the whole reason for the “else” keyword. When this loop is run, the “if” will be evaluated a number of times, and if it is ever true, the loop will break. A “break” means the “else” isn’t executed. Only if the “break” isn’t taken, that is, if the “if” is always false, will the “else” clause be run.
This is where the “else” keyword makes total sense. If you focus on the “if” statement, it is evaluated many times, and only if it is always false, will the “else” clause of the loop run. As the loop executes, the “if” becomes like a giant if/elif/elif ladder: each time around the loop adds another test of the condition, but only if all previous tests were false. The “else” clause caps it all off, just as it would in an explicit if/elif/else structure. And it works just like the explicit structure: if all of the conditions are false, then the “else” clause is run.
I think of the code above like this (informally):
if any(something_about(thing) for thing in container):
do_something(that_thing)
else:
no_such_thing()
This is the explanation that makes sense to me, because the “else” in my conceptual code is just like the “else” in the actual code. Maybe this isn’t a trivial explanation, but it’s one that makes sense to me, and I hope, to others.
Comments
Consider: Let's use 'break' to separate the loop structure from the condition test: Now let's do something in that second branch before we terminate the loop: And then merge the loop and the condition check back together while keeping the contents of the else clause: The else clause is the "other branch" of the implicit if statement in a while loop. The implicit if statement in a for loop is significantly less obvious, but it's still there, and so that construct inherits the else clause as well.
The reason break, return and exceptions bypass a loop's else clause is because they terminate the loop from a point that's already on the other branch of the implicit "do I go around again?" check.
Let me explain my initial confusion, as I think it will help your explanations.
The first time I saw it, I assumed it was a short-cut for this: This remains to me the most intuitive way of interpreting an unusual construct, and I still sometimes need to mentally correct myself when I use it.
The reason I bring this up is this: Consider the comment and identifier in the else clause in your first for-loop example: These are a little ambiguous. They don't distinguish between not finding it because (a) the list to search was empty, or (b) it wasn't a member the (possibly empty) list. The second interpretation is the correct one, but the first one could still be taken by someone who hasn't yet got it.
I suggest you choose examples that couldn't be misinterpreted this way to help people like the old me.
1. Whenever I see it, I usually require an extra mental cycle to remember what it does
2. Then newbie programmers see it, they usually don't understand it at all and have to read the tutorial or other explanations
As such IMHO this construct violates the Principle of Least Astonishment. Not something that Python tends to do much...
In fact, taking the text from "The classic (trivial) description" to "So it seems like the wrong word." and then showing the code snippet seems complete, and it has that "huh? - aha!" combo that makes it seem like a rewarding discovery to the reader/student.
Some advice from the C/C++ trenches, where bad use of keywords and operator overloads that don't make any bloody sense reign supreme: Don't spend too much time trying to rationalize a mis-choice of keyword. If you can't fix it, avoid it. If you can't avoid it, comment copiously.
In other languages, I've often written code like this. the "else" on a for loop allows this to be coded more compactly as: So the else clause on the loop allows you to avoid defining and then testing a "found" flag.
Even if it were not so unclear, the "found" flag is sometimes useful further down in the code, so I might end up using that anyway.
I think it must have been on analogy with try/except/else, where the "else" clause is the alternative to the "except" clause. A completely analogous construct for loops (the "for" or "while" loop) would have looked something like this, with a "break" statement being treated as if it raised a kind of exception. In this construct, the "else" clause is the alternative to the "broken" clause.
Actually, I sort of like the idea of a "broken" keyword. It would make looping constructs symmetrical with try/except/else, and it would make it obvious what the "else" in looping constructs is an alternative to.
Maybe we need a PEP to propose the addition of a "broken" keyword. :-)
for/while's "else" should have been called "if_no_break" and that's probably how it should be explained.
https://twitpic.com/4a52sh
Remember that Python provides not only a "for..else" looping construct, but also a "while..else" construct. Execution of the body of a "while" loop is determined by a loop condition, not by the contents of a container. And when we use "for" to iterate over the results returned by a generator, what we're doing is as much like testing a loop condition (while generator_function returns a result...) as it is like processing the items in a container.
So I don't think that we can specify our intuitive behavior of "else" in terms of the emptiness or non-emptiness of containers. We probably need to specify it this way: in a looping construct, an "else" clause executes if and only if the loop body is never entered. Or: iff the loop condition never evaluates to True.
In the case of a "while" loop, for example, that would mean: Actually, now that I think about it, that kind of behavior would be pretty nifty!
Consider what happens if the list is not empty:
Using else as the keyword I agree with others that I would expect the else to be triggered based on an empty list.
If this construct is to be made intuitive "else" should be renamed to "last" perhaps...
I hope we all agree that 1) enumerate is more readable, and 2) it only is because we understand what enumerate does.
Isn't the same true for "for/else"? Yes, you can replace "for/else" with an explicit "found" variable, but you're adding to the bulk of the code, increasing opportunities for errors, and not using one of the tools Python provides for concise code.
There is indenting inconsistency: There is the fact that you already have a condition being evaluated as an implicit part of the for: looping constructs boil down to the logical steps: It is implied, in most circumstances, that an "else" in that circumstance will relate to the implied if and not something contained within the body of the if.
It also contradicts the way that while...else works. A language which wishes to be consistent, should have for loops simply be semantic sugar for a complicated while loop.
# do something
else :
# the coder wants job security - to monopolize maintenance of the code
I was not trying to imply that the else only gets called for empty lists, and in fact explicitly said "iterating an empty container will trigger the else".
Having said that, I'm definitely leaning towards banning the construct in my development team because of the confusion it makes possible.
Wait, so there was confusion over the confusion over the confusion?
I think I'd better apologise for my piece in this, and step carefully away from this conversation!
Hope it's useful for someone: https://gist.github.com/1308847
Following on the comment by Steve Ferg, I would have suggested the following behaviour:
however, we would have a confusion with the current meaning of “else”, so using another keyword would (such as ‘empty’) be better suited.
the current syntax and the way ‘else’ - although elegant - works feels a bit useless because a similar result can be achieved with a try/except statement:
IMHO this makes it quite clear why the current meaning of “else” in a loop construct causes so much confusion and - may I say - disgust.
Currently so far, there is no syntaxically clean way to do something if a loop never ran ; the only way is to use an additional variable that gets set over and over on every iteration:
Add a comment: