You’ve probably already heard of iterable objects and iterators — and you’ve likely used them, too. These include any objects you can loop through using a for-loop, retrieving one element at a time.
(If you haven’t encountered them yet because you’re still at the beginning of your Python learning journey, it’s highly recommended to get familiar with these concepts as early as possible. Iterables and iterators are fundamental concepts and language features in Python, and you’ll come across them all the time.)
Now imagine this: you might want to write a function that accepts any iterable object — but fails or behaves incorrectly if the input isn’t iterable. So how can you reliably check if an object is truly iterable?
It’s commonly known that an object (let’s name it x) is iterable if it implements the __iter__ method. So when asked how to check if an object is iterable, the immediate answer might be to use the built-in hasattr(x, ‘__iter__’). This checks whether the object has an __iter__ method and returns True or False accordingly.
There’s another answer you might hear from those who are familiar with abstract base classes and the collections.abc module. They might suggest using isinstance(x, Iterable) to check whether x is an Iterable.
Of course, if these were the fully correct answers, there wouldn’t be much point in dedicating a whole blog post to the question. In reality, this is one of those “all bugs are insects, but not all insects are bugs” situations.
It’s true that any object that implements __iter__ is iterable. But the reverse is not necessarily true: not all iterable objects have an __iter__ method.
So which objects are iterable without defining __iter__?
The answer: those that implement __getitem__. This method accepts an index, slice, or key and returns the corresponding item.
At this point, someone more familiar with abstract base classes might say, “Alright, no problem — just check whether the object is of type
Sequence or
Mapping by calling isinstance(x, Sequence) or isinstance(x, Mapping).”
They might reason that both sequence and mapping types define
__getitem__, so this should cover it.
While this is technically true, it’s not the right answer — because both Sequence and Mapping also define __iter__.
To illustrate all of this, below is a program that defines a custom class named
Indexable where only
__getitem__ is implemented, and
__iter__ is not.
If you run this script, you’ll see that looping over an instance of this class still works just fine — even though
__iter__ is not defined. When comparing it with the selected sequence, mapping, and set objects, the differences can be analyzed in the table following the program codes.
|
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 |
from collections.abc import Iterable, Sequence, Mapping class Indexable: """Provides three elements accessible by index.""" def __init__(self, item1, item2, item3): self.a, self.b, self.c = item1, item2, item3 def __getitem__(self, index: int): d = {0: self.a, 1: self.b, 2: self.c} try: return d[index] except KeyError: raise IndexError("Index out of range") def check_iterability(x): print(f'{type(x).__name__}') print('\tIs instance of Iterable:', isinstance(x, Iterable)) print('\tIs instance of Sequence:', isinstance(x, Sequence)) print('\tIs instance of Mapping:', isinstance(x, Mapping)) print('\tDefines __iter__:', hasattr(x, '__iter__')) print('\tDefines __getitem__:', hasattr(x, '__getitem__')) # TESTS check_iterability(list()) check_iterability(range(1)) check_iterability(dict()) check_iterability(set()) check_iterability(Indexable(10, 20, 30)) for item in Indexable(10, 20, 30): print(item) # Output: # list # Is instance of Iterable: True # Is instance of Sequence: True # Is instance of Mapping: False # Defines __iter__: True # Defines __getitem__: True # range # Is instance of Iterable: True # Is instance of Sequence: True # Is instance of Mapping: False # Defines __iter__: True # Defines __getitem__: True # dict # Is instance of Iterable: True # Is instance of Sequence: False # Is instance of Mapping: True # Defines __iter__: True # Defines __getitem__: True # set # Is instance of Iterable: True # Is instance of Sequence: False # Is instance of Mapping: False # Defines __iter__: True # Defines __getitem__: False # Indexable # Is instance of Iterable: False # Is instance of Sequence: False # Is instance of Mapping: False # Defines __iter__: False # Defines __getitem__: True # 10 # 20 # 30 |
| Iterable | Sequence | Mapping | __iter__ | __getitem__ | |
| list | ✓ | ✓ | ✖ | ✓ | ✓ |
| range | ✓ | ✓ | ✖ | ✓ | ✓ |
| dict | ✓ | ✖ | ✓ | ✓ | ✓ |
| set | ✓ | ✖ | ✖ | ✓ | ✖ |
| Indexable | ✖ | ✖ | ✖ | ✖ | ✓ |
Because of cases like this, the official Python documentation emphasizes that the only reliable way to determine whether an object is iterable is to try calling iter(x). If x is not iterable, this will raise a TypeError.
So, if you want to safely and reliably check whether an object is iterable, the recommended approach is to call the built-in iter(x) function within a try…except block.
This topic is also covered in more detail in the e-book Python Knowledge Building Step by Step, with practical examples that help deepen your understanding.