Libor's blog


Mypy generics and subtypes

Published on Apr 07, 2022

Type Optional[bool] can be one of three values - None, True or False. Type bool can be one of True or False. So we can easily say that bool is subtype of Optional[bool], right? Let's see what happens when we start nesting these types.

Recently, I was writing a code that I was sure would pass mypy checks, but it didn't. Let's look at minimal example of such code:

from typing import List, Optional


def filter_out_nones(opt_bool_list: List[Optional[bool]]) -> List[bool]:
    ...


if __name__ == "__main__":
    bool_list: List[bool] = []
    bool_list = filter_out_nones(bool_list)  # mypy fails here

If you run mypy on a script like this, even without strict mode it outputs this error:

script.py:10: error: Argument 1 to "filter_out_nones" has incompatible type "List[bool]"; expected "List[Optional[bool]]"
script.py:10: note: "List" is invariant -- see https://mypy.readthedocs.io/en/stable/common_issues.html#variance
script.py:10: note: Consider using "Sequence" instead, which is covariant
Found 1 error in 1 file (checked 1 source file)

If my function can operate on list of Optional[bool], I would expect it to be able to also operate on bool. So what's the problem here?

To illustrate the issue, I will create a new generic class, where we can see more easily what's happening.

from typing import Generic, List, Optional, TypeVar


T = TypeVar("T")


class MyList(Generic[T]):
    ...


def filter_out_nones(opt_bool_list: MyList[Optional[bool]]) -> MyList[bool]:
    ...


if __name__ == "__main__":
    bool_list: MyList[bool] = MyList()
    bool_list = filter_out_nones(bool_list)

And see what's being substituted where. The function filter_out_nones accepts type MyList[T], where T = Optional[bool], which could also be written as U[T] = Optional[bool] and we are passing type MyList[T] where T = bool. Now, we can easily see, that U[T] != T, and even though bool is subtype of Optional[bool], it doesn't apply in general that type T is subtype of type U[T] and mypy doesn't do this kind of deep analysis. Therefor it just say we are passing incompatible types (and in a way its true).

Just for comparison - int is subtype of float (no nested generics) and it works:

from typing import List


def some_func(floats: List[float]) -> List[float]:
    ...



if __name__ == "__main__":
    some_func(list(range(3)))

How to solve this

To persuade mypy to believe us, simply use cast:

from typing import List, Optional, cast


def filter_out_nones(opt_bool_list: List[Optional[bool]]) -> List[bool]:
    ...


if __name__ == "__main__":
    bool_list: List[bool] = []
    bool_list = filter_out_nones(
        cast(List[Optional[bool]], bool_list)
    )  # everything's fine