Else Clauses on Loop Statements¶
Python’s loop statements have a feature that some people love (Hi!), some
people hate, many have never encountered and many just find confusing: an
else
clause.
This article endeavours to explain some of the reasons behind the frequent confusion, and explore some other ways of thinking about the problem that give a better idea of what is really going on with these clauses.
Reasons for Confusion¶
The major reason many developers find the behaviour of these clauses potentially confusing is shown in the following example:
>>> if [1]:
... print("Then")
... else:
... print("Else")
...
Then
>>> for x in [1]:
... print("Then")
... else:
... print("Else")
...
Then
Else
The if <iterable>
header looks very similar to the for <var> in
<iterable>
header, so it’s quite natural for people to assume they’re
related and expect the else
clause to be skipped in both cases. As the
example shows, this assumption is incorrect: in the second case, the else
clauses triggers even though the iterable isn’t empty.
If we then look at a common while
loop pattern instead, it just deepens
the confusion because it seems to line up with the way we would expect
the conditional to work:
>>> x = [1]
>>> while x:
... print("Then")
... x.pop()
... else:
... print("Else")
...
Then
Else
>>> if x:
... print("Then")
... else:
... print("Else")
...
Else
Here, the loop runs until the iterable is empty, and then the else
clause is executed, just as it is in the if
statement.
A different kind of else
¶
So what’s going on? The truth is that the superficial similarity between
if <iterable>
and for <var> in <iterable>
is rather deceptive. If
we call the else
clause on an if
statement a “conditional else”, then
we can look to try
statements for a different kind of else
clause,
a “completion clause”:
>>> try:
... pass
... except:
... print("Then") # The try block threw an exception
... else:
... print("Else") # The try block didn't throw an exception
...
Else
With a completion clause, the question being asked has to do with how
an earlier suite of code finished, rather than checking the boolean value
of an expression. Reaching the else
clause in a try
statement means
that the try block actually completed successfully - it didn’t throw
an exception or otherwise terminate before reaching the end of the suite.
This is actually a much better model for what’s going on in our for
loop,
since the condition the else
is checking for is whether or not the loop
was explicitly terminated by a break
statement. While it’s not legal
syntax, it may be helpful to mentally insert an except break: pass
whenever you encounter a loop with an associated else
clause in order
to help remember what it means:
for x in iterable:
...
except break:
pass # Implied by Python's loop semantics
else:
... # No break statement was encountered
while condition:
...
except break:
pass # Implied by Python's loop semantics
else:
... # No break statement was encountered
What possible use is the current behaviour?¶
The main use case for this behaviour is to implement search loops, where you’re performing a search for an item that meets a particular condition, and need to perform additional processing or raise an informative error if no acceptable value is found:
for x in data:
if acceptable(x):
break
else:
raise ValueError("No acceptable value in {!r:100}".format(data))
... # Continue calculations with x
But how do I check if my loop never ran at all?¶
The easiest way to check if a for
loop never executed is to use None
as a sentinel value:
x = None
for x in data:
... # process x
if x is None:
raise ValueError("Empty data iterable: {!r:100}".format(data))
If None
is a legitimate data value, then a custom sentinel object can be
used instead:
x = _empty = object()
for x in data:
... # process x
if x is _empty:
raise ValueError("Empty data iterable: {!r:100}".format(data))
For while
loops, the appropriate solution will depend on the details of
the loop.
But couldn’t Python be different?¶
Backwards compatibility constraints and the general desire not to change the language core without a compelling justification mean that the answer to this question is likely always going to be “No”.
The simplest approach for any new language to take to avoid the confusion encountered in relation to this feature of Python would be to just leave it out altogether. Many (most?) other languages don’t offer it, and there are certainly other ways to handle the search loop use case, including a sentinel based approach similar to that used to detect whether or not a loop ran at all:
result = _not_found = object()
for x in data:
if acceptable(x):
result = x
break
if result is _not_found:
raise ValueError("No acceptable value in {!r:100}".format(data))
... # Continue calculations with result
Closing note: Not so different after all?¶
Attentive readers may have noticed that the behaviour of while
loops
still makes sense regardless of whether you think of their else
clause as
a conditional else or as a completion clause. We can think of a while
statement in terms of an infinite loop containing a break
statement:
while True:
if condition:
pass # Implied by Python's loop semantics
else:
... # While loop else clause runs here
break
... # While loop body runs here
If you dig deep enough, it’s also possible to relate the completion clause
constructs in try
statements and for
loops back to the basic
conditional else construct. The thing to remember though, is that it is
only while
loops and if
statements that are checking the boolean
value of an expression, while for
loops and try
statements are
checking whether or not a section of code was aborted before completing
normally.
However, digging to that deeper level doesn’t really provide much more
enlightenment when it comes to understanding how the two different forms
of else
clause work in practice.