Why does mypy not consider a class as iterable if it has __len__ and __getitem__ but no __iter__

Viewed 2733

I was playing around with mypy and some basic iteration in Python and wrote the below code base:

from typing import Iterator
from datetime import date, timedelta

class DateIterator:
    def __init__(self, start_date, end_date):
        self.start_date = start_date
        self.end_date = end_date
        self._total_dates = self._get_all_dates()

    def _get_all_dates(self) -> Iterator[date]:
        current_day = self.start_date
        while current_day <= self.end_date:
            yield current_day
            current_day += timedelta(days=1)

    def __len__(self):
        print("Calling the len function...")
        return len(self._total_dates)

    def __getitem__(self, index):
        print(f"Calling getitem with value of index as {index}")
        return self._total_dates[index]

if __name__ == "__main__":
    date_iterator = DateIterator(date(2019, 1, 1), date(2019, 1, 15))
    for new_date in date_iterator:
        print(new_date)

    date_str = ",".join([str(new_date) for new_date in date_iterator])
    print(date_str)

    print(f"Checking the length of the collection {len(date_iterator)}")

    print(f"Checking if indexing works : {date_iterator[4]}")

Now to also play around with mypy i got the below issues:

iterator_test_with_getitem.py:30: error: Cannot assign to a type
iterator_test_with_getitem.py:30: error: "DateIterator" has no attribute "__iter__" (not iterable)
iterator_test_with_getitem.py:33: error: "DateIterator" has no attribute "__iter__" (not iterable)
Found 3 errors in 1 file (checked 1 source file)

Can someone please guide me that if an object is iterable with the addition of __len__ and __getitem__ methods then why is mypy complaining when it has no iter method

Also can someone please tell me what the issue with line 30 is . I don't find any logical explanation of that error as well.

1 Answers

Mypy -- and PEP 484 type checkers in general -- define an iterable to be any class that defines the __iter__ method. You can see the exact definition of the Iterable type in Typeshed, the collection of type hints for the standard library: https://github.com/python/typeshed/blob/master/stdlib/3/typing.pyi#L146

The reason why Iterable is defined in this way and excludes types that only define __getitem__ and __len__ is because:

  1. The only way to say that a type must either implement __iter__ or implement __getitem__/__len__ is to use a Union -- and defining Iterable to be a union complicates life a bit for anybody who wants to make extensive use of the Iterable type in their own code.

  2. Conversely, it's trivial for a class that defines __getitem__ and __len__ to define their own __iter__ method. For example, you could do something as simple as this:

    def __iter__(self) -> Iterator[date]:
        for i in range(len(self)):
            yield self[i]
    

    Or alternatively, something like this (assuming you fixed your constructor so self._total_dates is a list, not a generator):

    def __iter__(self) -> Iterator[date]:
        return iter(self._total_dates)
    

So, given this cost-benefit tradeoff, it makes sense to define Iterable to just be any object that implements __iter__. It's not much of a burden for people who are defining custom classes and simplifies life for people who want to write functions manipulating iterables.

Related