Python For Else

I was recently chatting with a friend who introduced me to a feature of the python language which I had never seen before, and it’s not new! This is rather an infrequent experience recently, so I was intrigued and have been thinking a bit about how it can be used.

Let’s start of with a brief introduction to the feature, it’s the “for else” statement, basically a normal for loop with a following else: block.

It’s pretty easy to understand with a quick example:

>>> my_list = [1, 2, 3]
>>> for i in my_list:
...     print(i)
...     if i == 2:
...         break
... else:
...     print('iterated over entire list')
1
2
>>> for i in my_list:
...     print(i)
...     if i == 4:
...         break
... else:
...     print('iterated over entire list')
1
2
3
iterated over entire list

The else block is only executed if the for loop completes (i.e. doesn’t exit early, e.g. with a break or return)

Simple Example

Let’s take a look at an example use case which this can help with.

Say we have an iterable of dictionaries, and we want to search for the dictionary which matches some criteria, but also have a default if we don’t find it:

>>> list_of_dicts = [
...     {'id': 1, 'name': 'Alf'},
...     {'id': 2, 'name': 'Billy'},
...     {'id': 3, 'name': 'Chris'},
...     {'id': 4, 'name': 'Dave'},
... ]
>>> def get_dict_by_name(name):
...     for d in list_of_dicts:
...         if name == d['name']:
...             return d
...     else:
...         return {'id': -1, 'name': 'Default'}
>>> get_dict_by_name('Chris')
{'id': 3, 'name': 'Chris'}
>>> get_dict_by_name('Scott')
{'id': -1, 'name': 'Default'}

Real World Example

A common place where this pattern can emerge would be when interacting with a REST API, where one might have a resource such as a user

We might want to search to see if a user already exists, and if not, create them:

>>> list_of_users = ...  # GET request to `/users`
>>> def get_dict_by_name(name):
...     for d in list_of_users:
...         if name == d['name']:
...             return d
...     else:
...         new_user = ...  # POST request to `/users`
...         return new_user

Ideally we’d be able to provide some sort of the query filter (parameter) to the API to limit the number of paginated GET requests we’d need to do to get the user we’re looking for, but often API’s don’t give us every capability we’d like, and then we have to work around limitations

Reusable functionality

from typing import Hashable, Any, Iterable

def lookup_in_iterable_of_dicts(
        lookup_key: Hashable,
        lookup_value: Any,
        list_of_dicts: Iterable[dict]
) -> Optional[dict]:
    for d in list_of_dicts:
        if d[lookup_key] == lookup_value:
            return  d
    else:
        return None

Note: prior to 3.9, or 3.7 with from __future__ import annotations you would need to use typing.Dict rather than dict in the type hint

Short alternative

A one line alternative to the above structure would be to construct a generator comprehension (adapted from this stackoverflow post)

from typing import Hashable, Any, Iterable

def lookup_in_iterable_of_dicts(
        lookup_key: Hashable,
        lookup_value: Any,
        list_of_dicts: Iterable[dict]
) -> Optional[dict]:
    return next((d for d in list_of_dicts if d[lookup_key] == lookup_value), None)

What else might I be unaware of?

Having realised that I had never seen what seemed like a fairly basic and potentially useful feature, it made me wonder… what else I might have missed? On my search I found this python docs page on compound statements which having read though, introduced me to:

with A() as a, B() as b:
    SUITE

which is syntactic sugar for:

with A() as a:
    with B() as b:
        SUITE

where SUITE is the code you want to run within the context manager.

So that page seems like a good read to check if there is anything else you might be unfamiliar with, perhaps the relatively new async for statement