With for-loops and comprehensions at our fingertips, the utility of these lower level iteration protocols may not be obvious. To demonstrate a more concrete use, here's a little utility function which, when passed an iterable object, returns the first item from that series or, if the series is empty, raises a ValueError:
>>> def first(iterable):
... iterator = iter(iterable)
... try:
... return next(iterator)
... except StopIteration:
... raise ValueError("iterable is empty")
...
This works as expected on any iterable object, in this case both a list and a set:
>>> first(["1st", "2nd", "3rd"])
'1st'
>>> first({"1st", "2nd", "3rd"})
'1st'
>>> first(set())
Traceback (most recent call last):
File "./iterable.py", line 17, in first
return next(iterator)
StopIteration
During handling of the above exception, another exception occurred:
Traceback (most recent call last):
File "<stdin>", line 1, in <module>
File "./iterable.py", line 19, in first
raise ValueError("iterable is empty")
ValueError: iterable is empty
It's worth noting that the higher-level iteration constructs, such as for-loops and comprehensions, are built directly upon this lower-level iteration protocol.