CodeQL documentation

__iter__ method returns a non-iterator

ID: py/iter-returns-non-iterator
Kind: problem
Severity: error
Precision: high
Tags:
   - reliability
   - correctness
Query suites:
   - python-security-and-quality.qls

Click to see the query in the CodeQL repository

The __iter__ method of a class should always return an iterator.

Iterators must implement both __next__ and __iter__ for Python 3, or both next and __iter__ for Python 2. The __iter__ method of the iterator must return the iterator object itself.

Iteration in Python relies on this behavior and attempting to iterate over an instance of a class with an incorrect __iter__ method can raise a TypeError.

Recommendation

Make sure the value returned by __iter__ implements the full iterator protocol.

Example

In this example, we have implemented our own version of range, extending the normal functionality with the ability to skip some elements by using the skip method. However, the iterator MyRangeIterator does not fully implement the iterator protocol (namely it is missing __iter__).

Iterating over the elements in the range seems to work on the surface, for example the code x = sum(my_range) gives the expected result. However, if we run sum(iter(my_range)) we get a TypeError: 'MyRangeIterator' object is not iterable.

If we try to skip some elements using our custom method, for example y = sum(my_range.skip({6,9})), this also raises a TypeError.

The fix is to implement the __iter__ method in MyRangeIterator.

class MyRange(object):
    def __init__(self, low, high):
        self.low = low
        self.high = high

    def __iter__(self):
        return MyRangeIterator(self.low, self.high)

    def skip(self, to_skip):
        return MyRangeIterator(self.low, self.high, to_skip)

class MyRangeIterator(object):
    def __init__(self, low, high, skip=None):
        self.current = low
        self.high = high
        self.skip = skip

    def __next__(self):
        if self.current >= self.high:
            raise StopIteration
        to_return = self.current
        self.current += 1
        if self.skip and to_return in self.skip:
            return self.__next__()
        return to_return

    # Problem is fixed by uncommenting these lines
    # def __iter__(self):
    #     return self

my_range = MyRange(0,10)
x = sum(my_range) # x = 45
y = sum(my_range.skip({6,9})) # TypeError: 'MyRangeIterator' object is not iterable

References