Why Python's for-else Clause Makes Perfect Sense, but You Still Shouldn't Use It

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 break statement. 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

The 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)

The 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.

Conclusion

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.

Suggested Articles

If you enjoyed this article, then you might also enjoy these related ones.

Recreating Python's Slice Syntax in JavaScript Using ES6 Proxies

By Evan Sangaline
on June 28, 2018

A gentle introduction to JavaScript proxies where we use them to recreate Python's extended slice syntax.

Read more

What's New in Exodus 2.0

By Evan Sangaline
on March 8, 2018

A tour of the new features introduced in Exodus version 2.0.

Read more

JavaScript Injection with Selenium, Puppeteer, and Marionette in Chrome and Firefox

By Evan Sangaline
on December 29, 2017

An exploration of different browser automation methods to inject JavaScript into webpages.

Read more

Comments