DEV Community

Pavel Morava
Pavel Morava

Posted on

On iterators and iterables in Python

In this, hopefully, very short write-up I will discuss a few facts a Pythonista should know about...

Drums roll!

Iterator vs Iterable? The rap battle begins!

When I started with Python, I used those words interchangeably because they are so similar. Without any technical details, allow me to share my simplistic guide how to decide which is which.

But first of all, we will prepare the stage. See how our contestants enter proudly.

iterable = [1,2,3]
iterator = iter(iterable)
Enter fullscreen mode Exit fullscreen mode

For future reference, remember I used a list of three integers as iterable and created my iterator from this list. In next text, I will call them accordingly and explain

So which is which?

If the object in question do not raise an exception after the next function is applied, call it an iterator.

# The iterator won't complain
assert next(iterator) == 1
Enter fullscreen mode Exit fullscreen mode

On the other hand, the following code will fail. Call it iterable if, of course, the iter function works on it. In practice, the term iterable depicts objects from which one can obtain an iterator.

Let's make some tests.

try:
    next(iterable)
except TypeError:
    print("prints: The type list is clearly not an iterator!")

try:
    iterable.__next__
except AttributeError:
    print("prints: No __next__ method implemented!")

iterable.__iter__
print("prints: No complains here as this iterable implements __iter__ method!")
Enter fullscreen mode Exit fullscreen mode
prints: The type list is clearly not an iterator!
prints: No __next__ method implemented!
prints: No complains here as this iterable implements __iter__ method!
Enter fullscreen mode Exit fullscreen mode

To put it simply, it is just a question of two dunder methods and their implementations:

Two culprits

def __next__(self)
def __iter__(self)
Enter fullscreen mode Exit fullscreen mode

I don't intend to explain what the methods do. You may ask in the comment section if you need more info. Suffice it to say that these are equivalent:

next(Something)  equals to Something.__next__()
iter(Something)  equals to Something.__iter__()
Enter fullscreen mode Exit fullscreen mode

The curse of mutability

In this particular case, I created the iterator from a list, which is mutable by default. Our two inspected objects (iterable and iterator) are linked closely. If the first, understand the iterable changes, the latter will follow its lead. Behold!

iterable[1] = "changed"
assert next(iterator) == "changed"
Enter fullscreen mode Exit fullscreen mode

The everlasting bound

The curious mind may wonder what happens if we destroy the original object, the iterable. If they both share a link, deleting first one should invalidate the latter one, the iterator.

In reality, even removing the iterable from our namespace doesn't damage the bound these two share. Being once again technical, the garbage collector cannot remove the iterable since there is one lasting reference to it, held by the iterator.

Just check it!

del iterable
Enter fullscreen mode Exit fullscreen mode
assert next(iterator) == 3
Enter fullscreen mode Exit fullscreen mode

The end of the story

We had three numbers in our iterable, so the iterator has to be exhausted by now. As there is nothing to pull, Python raises the StopIteration exception to indicate we ask too much.

This is a crucial part to understand. After the iteration is over, the typical iterator doesn't start from the beginning. The story ends here.

try:
    next(iterator)
except StopIteration:
    print("prints: THE END!")
Enter fullscreen mode Exit fullscreen mode
prints: THE END!
Enter fullscreen mode Exit fullscreen mode

Discussion (0)