By Andre Perunicic | August 28, 2018
An interesting (and somewhat obscure) feature of Python is being able to attach an else block to a loop.
The basic idea is that the code in the else block runs only if the loop completes without encountering a
Here’s a trivial example in the form of a password guessing game:
for i in range(3): password = input('Enter password: ') if password == 'secret': print('You guessed the password!') break else: print('3 incorrect password attempts')
If you make an incorrect guess 3 times in a row, then the message from the else block will be printed:
Enter password: password1 Enter password: password2 Enter password: password3 3 incorrect password attempts
If you guess the correct password—secret—in your first three tries, then the message from the else block will not be printed:
Enter password: password Enter password: secret You guessed the password!
As you’ve seen here, the loop-else construct can be explained in just about no time at all, but there’s still a large amount of confusion about its meaning all over the web. This is hardly surprising given that formulaic explanations such as the one above don’t reveal any of the underlying intuition and logic behind the statement. After all, if there’s an else block following a loop, is there an an actual if statement that it can be associated with? In the rest of this article, I’ll show you that while-else and for-else actually make perfect sense, and then argue why you should use them as rarely as possible anyway.
Understanding the loop-else Construct
To understand why while-else works the way that it does, let’s transform it into equivalent code that places its else block in an if-else clause.
Raymond Hettinger, one of the core Python developers, did exactly that in a tweet where he posted C code with
goto statements to illustrate the same point.
Let’s capture the spirit of his tweet directly in Python instead of C.
while True: # Do some work here, possibly `break`. if condition: continue else: # Do stuff if the condition isn't met. break
condition here is just the looping condition, which the else block is directly attached to.
Logically, the blocks in this loop correspond clearly to the blocks of an analogous while-else clause.
while condition: # Do some work here, possibly `break`. else: # Do stuff if the condition isn't met.
In the case of for-else the explanation is a bit more abstract because a StopIteration is used internally, but we can still construct a similar if-else block that functions in the same way.
while True: # The looping condition is `True` until `StopIteration` is encountered. condition = True try: item = next(iterable) # Do some work here, possibly `break`. except StopIteration: condition = False # Handle the cases of the condition being `False` and the `while-else` block. if condition: continue else: # Do stuff if the condition isn't met. break
This might be straying further from the actual internal implementation of for-else loops, but it makes the analogy with for-else fairly clear. In both cases, we run the code from the else block when the condition that continues the loop becomes falsey.
Why Not to Use loop-else
While loop-else has a perfectly reasonable explanation and can cut down on some boilerplate, you should still avoid using it if possible. The simple reason is that it can often be easily replaced by more fundamental and universally understandable Python constructs. It’s easy to be a purist and ignore social coding pressures when making your software, but if you want other programmers–or you in the future–to easily understand what you’ve written, it may be a good idea to take them under consideration. Even Python’s retired creator Guido van Rossum has stated that he would not include loop-else in Python if he had to do it over.
I’ll illustrate my point by actually implementing a sketch of the prototypical for-else clause application: incrementally searching through an iterable. With for-else, this might look as follows.
for song in songs: if song.name == next_song_name: next_song = song break else: next_song = download_song(next_song_name)
We are iterating over a made up iterable
songs here until we find one whose name matches the “next song,” or until we exhaust the list of available songs and downloaad the missing one.
This can be implemented even more simply using Python’s built-in next() and filter() functions.
matching_songs = filter(lambda song: song.name == next_song_name, songs) next_song = next(matching_songs, None) or download_song(next_song_name)
filter() function creates an iterator of songs with matching names, and
next() will either return the first match or
None if there aren’t any.
Note that the use of an iterator here ensures that the
songs list will only explored as far as necessary to find a match, so the efficiency here is comparable to the for-else code.
This example is admittedly a bit contrived, but there are pretty much always more readable alternatives to using Python’s loop-else constructs. Using more common constructs will prevent someone from needing to think too hard about your code while reading it, and maximizing understandability is as decent of a metric as any for deciding how to code something.
We’ve seen that Python has more uses for the else keyword than a simple if-else clause. In particular, we provided an intuitive explanation for the oft-misunderstood while-else and for-else constructs. While these can sometimes lead to shorter and more elegant code, I argued that their rarity and unusual semantics justify carefully considering whether an alternative is possible.
This was just a quick note about a neat tweet I encountered, but we publish more in-depth pieces, including various tutorials and explanations, on the Intoli blog. Consider subscribing to our mailing list and leaving your thoughts in the comment below.
If you enjoyed this article, then you might also enjoy these related ones.