Псевдонимы типов и генерики в Python

Как исправить загадочную ошибку Python Mypy: expects no type arguments, but 1 given [type-arg]

Приходилось ли вам слышать про псевдонимы типов в Питоне?

На первый взгляд, теория выглядит сложной и абстрактной. Давайте разберемся на элементарном примере.

Представьте, что у вас список животных. И у вас есть функция escaped() применимая к списку любых животных.

Но по каким-то причинам вам нужны специализированные списки - список птиц, список рептилий и т.д.

И вам хочется использовать проверки типов, чтобы нельзя было добавить птицу в список рептилий.

Я в курсе про летающих динозавров, но давайте без педантизма, тем более что летающие динозавры - не птицы.

Пишем простейших код.. и ловим совсем невнятную ошибку:

from typing import List


class Animal:
    escaped: bool


class Bird(Animal):
    pass


class AnimalList(List[Animal]):
    def escaped(self):
        return [mammal for mammal in self if mammal.escaped]


a = AnimalList()
a.append(Bird())  # ok


class Zoo:
    aviary: AnimalList[Bird]  # Mypy: "AnimalList" expects no type arguments, but 1 given [type-arg]

Что это за [type-arg] и почему нет проблем с List[Bird] но нельзя AnimalList[Bird]?

List[Animal] создает список для конкретного класса, в отличие от исходного генерика list.

И уже просто негде указывать тип элементов для AnimalList - это уже не генерик.

Решение элементарно - надо создать alias для типа Animal. Тогда в AnimalList останется возможность указать тип элементов.

from typing import List, TypeVar


class Animal:
    escaped: bool


AnimalSubClass = TypeVar("AnimalSubClass", bound=Animal)


class Bird(Animal):
    pass


class Reptile(Animal):
    pass


class AnimalList(List[AnimalSubClass]):
    def escaped(self):
        return [mammal for mammal in self if mammal.escaped]


class Zoo:
    aviary: AnimalList[Bird]


aviary = Zoo()
aviary.aviary.append(Bird())  # ok
aviary.aviary.append(Reptile())  # Mypy: Argument 1 to "append" of "list" has incompatible type "Reptile"; expected "Bird" [arg-type]

Если вам это кажется абстрактным вопросом без практического применения, посмотрите на мой код использующий type alias точно для такой ситуации.

Если вам интересно, что за аргумент bound в TypeVar в коде выше, то это просто заставляет определяемый type alias принимать как Animal так и любого наследника Animal.