Generic type aliasing in Python

How to fix mysterious Python error Mypy: expects no type arguments, but 1 given [type-arg]

Have you ever heard of generic type aliases?

At first glance, the theory behind them may seem complicated, but let me give you a practical example to make things easier to understand.

Imagine you have some generic list of animals. For example to encapsulate a function escaped() that apply to list of any animals.

Now let’s say that you want to create specialized list of birds, list of reptiles, and so on.

The problem is that you still want to use the common code for a generic list of animals. You also want to make sure that the types of animals in each list are checked, so that mypy won’t allow you to add a bird to the list of reptiles.

Alright, so I’m familiar with dinosaurs and all that, but let’s not get too deep into it and just keep things simple for the sake of the conversation. We don’t need to generalize everything over millions of years, ya know? After all flying dinosaurs are not birds.

Write extremely simple code.. and hit some crazy mypy error message:

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]

WTF? What is [type-arg] and why we can use List[Bird] but not AnimalList[Bird]?

When you use List[Animal] you actually use list with specific items types and without type argument that has its parent, generic list.

That’s why now there is just no place where to specify type for AnimalList items, this is not generic list anymore.

The solution is extremely simple. Create type alias for Animal. In this case AnimalList still expect items type argument.

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]

If you think this is too abstract problem, you can look into my code where I use this technique.

Curious about bound argument of TypeVar in the code above? It just means that type specified by this alias could be Animal or subclass of Animal.