Python For Else Construct: A Deep Dive

Can you use else in a for loop in Python? The answer is a resounding yes! I know it sounds crazy. The use of else after for and while loops in Python is not a widely known idiom. The use of the for-else construct is often misunderstood and overlooked.

For those who are coming from other programming languages, the syntax may be jarring. The fault is not on them though, the else clause is mostly associated only with the if statement.

But hold on, don’t jump to conclusions just yet! The else clause in loops may seem strange at first, but it can be a useful tool in your Python toolkit.

TL;DR

  • Think of else in a for-else construct as “no break”.
  • Consider using else with for or while loops instead of a flag variable to handle function break out of the loop.
  • An else clause is only useful with a preceding break in a loop. In other words, it’s pointless to use the for-else construct without a break statement.

What is for else in Python

Even today, the use of the for-else construct in Python remains largely unpopular. It often confuses even seasoned Python programmers.

Here’s a trick to remember — the else in a for-else construct can be remembered as “no break”.

It basically handles cases where no break statement is being executed within a loop. Easy, right?

But, when would you use the for-else construct in Python?

When and how to use else in a for loop

A common use case is to go through a loop until something is found. When we find what we want, we would then break out of the loop and handle that case accordingly.

In short, we need to determine which case has happened:

  1. We found the target (break out early)
  2. We finished the loop without finding the target

One common method is to create a flag variable that will let us do a check later to see how the loop was exited.

Now, let’s take a look at an example. Say you want to find the index of the first occurrence of a target in a sequence. You can do this using a flag variable:

def find(seq, target):
    """Find the index of the first occurrence of `target` in `seq`."""

    found = False  # a flag variable
    for i, value in enumerate(seq):
        if value == target:
            found = True
            break

    if not found:
        return -1

    return i

In practice, you would want to use Python’s built-in find method instead

Although this example is a little bit simplistic, code like this sometimes intermesh with other more complex code and there’s no shortcut out (i.e. can’t return the result immediately).

However, using the for-else construct, we can instead have:

def find(seq, target):
    """Find the index of the first occurrence of `target` in `seq`."""

    for i, value in enumerate(seq):
        if value == target:
            break

    else:  # no break
        return -1
    return i

Again in practice, you would want to use Python’s built-in find method instead

Perhaps looking more elegant, this can simplify your code and improve readability instead of using a flag variable with an additional if check.

In short, the else clause in a for loop can be used to execute a block of code when the loop has finished running without encountering a break statement.

For fun: benchmark

As I was down this rabbit hole, I got a bit curious about how these 4 different methods would perform. With some semantical differences, these functions can be used to find the index of the first occurrence of a character within a string.

Here’s what I ran:

import cProfile

# finds the index of a target in a sequence with a flag variable
cProfile.run("""
def flag_var_find(seq, target):    
    found = False
    for i, value in enumerate(seq):
        if value == target:
            found = True
            break
    
    if not found:
        return -1
    return i

flag_var_find('a' * 100_000_000, 'b')
""")

# finds the index of a target in a sequence using a for-else construct
cProfile.run("""
def else_find(seq, target):
    for i, value in enumerate(seq):
        if value == target:
            break

    else:
        return -1
    return i
else_find('a' * 100_000_000, 'b')
""")

# finds the index of a target in a sequence using a generator expression
cProfile.run("""
next((i for i, char in enumerate('a' * 100_000_000) if char == 'b'), -1)
""")

# find the index of the first occurrence of a substring within a string
cProfile.run("""
('a' * 100_000_000).find('b')
""")

You can copy and paste this into your ipython notebook. Note: strings are a special type of Sequence.

Here are the results:

      4 function calls in 6.462 seconds

Ordered by: standard name

ncalls  tottime  percall  cumtime  percall filename:lineno(function)
      1    0.019    0.019    6.462    6.462 <string>:1(<module>)
      1    6.443    6.443    6.443    6.443 <string>:2(flag_var_find)
      1    0.000    0.000    6.462    6.462 {built-in method builtins.exec}
      1    0.000    0.000    0.000    0.000 {method 'disable' of '_lsprof.Profiler' objects}

      4 function calls in 5.973 seconds

Ordered by: standard name

ncalls  tottime  percall  cumtime  percall filename:lineno(function)
      1    0.008    0.008    5.973    5.973 <string>:1(<module>)
      1    5.965    5.965    5.965    5.965 <string>:2(else_find)
      1    0.000    0.000    5.973    5.973 {built-in method builtins.exec}
      1    0.000    0.000    0.000    0.000 {method 'disable' of '_lsprof.Profiler' objects}

      5 function calls in 5.763 seconds

Ordered by: standard name

ncalls  tottime  percall  cumtime  percall filename:lineno(function)
      1    0.007    0.007    5.763    5.763 <string>:1(<module>)
      1    5.756    5.756    5.756    5.756 <string>:2(<genexpr>)
      1    0.000    0.000    5.763    5.763 {built-in method builtins.exec}
      1    0.000    0.000    5.756    5.756 {built-in method builtins.next}
      1    0.000    0.000    0.000    0.000 {method 'disable' of '_lsprof.Profiler' objects}

      4 function calls in 0.012 seconds

Ordered by: standard name

ncalls  tottime  percall  cumtime  percall filename:lineno(function)
      1    0.007    0.007    0.012    0.012 <string>:1(<module>)
      1    0.000    0.000    0.012    0.012 {built-in method builtins.exec}
      1    0.000    0.000    0.000    0.000 {method 'disable' of '_lsprof.Profiler' objects}
      1    0.004    0.004    0.004    0.004 {method 'find' of 'str' objects}

Subtly, we can see that using the for-else construct is just a tiny bit faster than the flag variable way.

On the other hand, using Python’s built-in string find method is significantly faster for this specific question. However, it will not work in the cases of other Sequence Types like Lists or Tuples.

💡
Jump into the rabbit hole of List vs. Tuple vs. Sets.

Using else with while loops

Here’s a version of the same find function using a while loop instead of a for loop:

def find(seq, target):
    """Find the index of the first occurrence of `target` in `seq`."""

    i = 0
    while i < len(seq):
        if seq[i] == target:
            return i
        i += 1
    else:
        return -1

When not to use the else clause in a loop

Simple. It’s pointless to use a for-else construct without a preceding break statement in a loop.

How to use else with a try-except block

Loops aside, the else clause can also be used with a try-except block to run code when no exceptions were raised in the try block.

Similar to the for-else construct, the try-else construct is useful to distinguish between:

  1. The code ran successfully without any exception
  2. The code ran into an exception
try:
	book = find_book(id=1)

except BookNotFoundError:
    # handle the exception
	print("Book not found! :(")

else:
    # run this block if no exception was raised
	send_slack_notification(book)

finally: 
	# this block is always executed
	close_db_connection()

In this case, the code in the else block will only run if the code in the try block was executed successfully, without any exceptions.

In summary, consider using the try-else construct to separate the normal execution path from the error handling code.

Do not confuse else with finally in try blocks

  • try-else — executes if there’s NO exception
  • try-finally — always gets executed regardless of exception
💡
Tip: use Python Context Manager (with statement) to encapsulate the use of try-finally instead when it comes to exception handling.

What’s Next

To summarize, the else clause in Python provides a way to execute code after a loop has completed execution without encountering a break statement.

I owe my thanks to this great talk by Raymond Hettinger, titled “Transforming Code into Beautiful, Idiomatic Python”. In the talk, he briefly spoke about the history of the for-else construct. I highly recommend watching it — I’ve been using Python for 5+ years and I still learn many great tidbits in such a short time from him.

I personally would use the for-else construct whenever it makes sense. Having that said, I would recommend trying to get your team to be on the same page before adopting this widely unpopular construct.

Lastly, if you’re interested to dive deeper into this rabbit hole, check out [Python-ideas] Summary of for...else threads.

Hosted on Digital Ocean.