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