Search code examples
pythontypesiteratorgenerator

Type annotation in a filter() function over a custom generator


could you help me understand why I am getting the TypeError: 'type' object is not subscriptable error with the code below?

Maybe I'm getting this wrong, but as I understood the Color type annotation in the filter() function is saying that the function will result in an Iterable of Color , which is exactly what I want. But when I try to annotate the function I get the error. ( but the waty, how come a type annotation is preventing the program to run? I thought that type hints in in Python would just matter inside your IDE, not in runtime).

Any light on this would be much appreciated.

# -*- coding: utf-8 -*-
from __future__ import annotations
from typing import TypeVar, Any, Generic, Iterator, Iterable
from abc import ABC, abstractmethod
from dataclasses import dataclass

T = TypeVar('T', bound=Any)
I = TypeVar('I', bound=Any)

class AbstractGenerator(ABC, Iterator[T], Generic[T, I]):
    def __init__(self):
        super().__init__()

        self._items = None
        self._next_item = None

    @property
    def items(self) -> Any:
        return self._items

    @items.setter
    def items(self, items: Any) -> AbstractGenerator:
        self._items = items

        return self

    @property
    def next_item(self) -> Any:
        return self._next_item

    @next_item.setter
    def next_item(self, next_item: Any) -> AbstractGenerator:
        self._next_item = next_item

        return self

    @abstractmethod
    def __len__(self) -> int:
        pass

    @abstractmethod
    def __iter__(self) -> Iterable[T]:
        pass

    @abstractmethod
    def __next__(self) -> Iterable[T]:
        pass

    @abstractmethod
    def __getitem__(self, id: I) -> Iterable[T]:
        pass

ColorId = int

@dataclass(frozen=True)
class Color:
    id: ColorId
    name: str

class MyColorsGenerator(AbstractGenerator[Color, int]):
    def __init__(self):
        super().__init__()
        
        self._colors: list[Color] = []
        self._next_color_index: int = 0 #None
        
    @property
    def colors(self) -> list[Color]:
        return self._colors
        
    @colors.setter
    def colors(self, colors: list[Color]) -> MyColorsGenerator:
        self._colors = colors
        
        return self
    
    @property
    def next_color_index(self) -> int:
        return self._next_color_index

    @next_color_index.setter
    def next_color_index(self, next_color_index: int) -> MyColorsGenerator:
        self._next_color_index = next_color_index
        
        return self
        
    def add_color(self, color: Color) -> MyColorsGenerator:
        self.colors.append(color)
        
        return self
        
    def __len__(self) -> int:
        return len(self.colors)

    def __iter__(self) -> Iterable[Color]:
        return self

    def __next__(self) -> Iterable[Color]:
        if self.next_color_index < len(self.colors):
            self.next_color_index += 1

            return self.colors[self.next_color_index - 1]
        
        else:
            raise StopIteration

    def __getitem__(self, id: ColorId) -> Iterable[Color]:
        return list(filter[Color](lambda color: color.id == id, self.colors))   
        
colors_generator: MyColorsGenerator = MyColorsGenerator()

colors_generator \
    .add_color(Color(id=0, name="Blue")) \
    .add_color(Color(id=1, name="Red")) \
    .add_color(Color(id=2, name="Yellow")) \
    .add_color(Color(id=3, name="Green")) \
    .add_color(Color(id=4, name="White")) \
    .add_color(Color(id=5, name="Black"))

# This results in: TypeError: 'type' object is not subscriptable
#colors: Optional[list[Color]] = list(filter[Color](lambda color: color.id == 4, colors_generator))

# This works, notice the only thing I did was to remove the type annotation for the expected generic type ([Color])    
colors: Optional[list[Color]] = list(filter(lambda color: color.id == 4, colors_generator))
print(colors)

Solution

  • The issue is that generics aren't a language-level addition, but a library one. Specifying the generic type parameters actually employs the same [] operator you use for item access in collections, except it is defined on the metaclass. For this reason the generics syntax originally only worked with specific classes in the typing module (typing.List[int], typing.Dict[str, str], etc.). Since python3.9, however, some common classes from the standard library have been extended to support the same operation, for brevity, like list[int], dict[str, str]. This is still NOT a language feature, and most classes in the standard library do not implement it. Moreover, as you've rightfully noticed, these annotations carry (almost) no meaning for the interpreter, and are (mostly) just there for the ide. Among other things, that implies that you don't instantiate generic classes as specialized generics (list() is correct, list[int]() is legal, but pointless and considered a bad practice). filter is a class in the standard library, which does not provide the generic-aliasing [] operation, so you get the error that applying it is not implemented ("'type' object is not subscriptable", filter is an instance of type, and [] is the subscription operator). Python as the language does not understand the concept of a generic, and so it cannot give you a better error message like "'filter' is not a generic class". Even if it was, however, you shouldn't have invoked it this way.

    A special note should be made about generic functions. They CANNOT be explicitly supplied with generic parameters. So, if instead of filter we were talking about some function like:

    T = typing.TypeVar("T")
    
    def my_filter(f: typing.Callable[[T], bool], seq: list[T]) -> list[T]:
        ...
    

    , there would have been no way to explicitly tell you're interested in my_filter[Color].

    TL;DR: filter is not a generic class in terms of type annotations, so it does not support the [] operation