I've created the following example:
from typing import List, Sequence
class Circle:
pass
def foo(circle: Circle) -> Sequence[Circle]:
return_value: List[Circle] = [circle]
return return_value
def bar(circle: Circle) -> List[Sequence[Circle]]:
# Incompatible return value type (got "List[List[Circle]]", expected "List[Sequence[Circle]]")
return_value: List[List[Circle]] = [[circle]]
return return_value
Why is it okay to return a List[Circle]
when it's expecting a Sequence[Circle]
, but not a List[List[Circle]]
when it's expecting a List[Sequence[Circle]]
?
More specifically, why is this not okay when the value is a return value? I think I understand why it's not okay as a parameter, but I don't get why this value is not accepted as a return value.
The docs give a great example displaying why List
s are invariant:
class Shape:
pass
class Circle(Shape):
def rotate(self):
...
def add_one(things: List[Shape]) -> None:
things.append(Shape())
my_things: List[Circle] = []
add_one(my_things) # This may appear safe, but...
my_things[0].rotate() # ...this will fail
Here, the idea is if you take your List[Subclass]
and pass it to something that thinks it's a List[Superclass]
, the function can edit your List[Subclass]
so that it contains Superclass
elements, so it becomes a List[Superclass]
after the function is run.
However, as a return value, I don't see why this is an issue. Once it exits that function, everyone will treat it as a List[Sequence[Circle]]
, which it is, so there should be no issues.
Once again, while typing up this question, I think I have figured out an answer to it.
Consider the following case:
from typing import List, Sequence
class Circle:
pass
def baz(circle_list_matrix: List[List[Circle]]) -> List[Sequence[Circle]]:
# Incompatible return value type (got "List[List[Circle]]", expected "List[Sequence[Circle]]")
return circle_list_matrix
Here, Mypy is absolutely right to raise the error, because the other functions that are using the circle_list_matrix
may depend on it being a List[List[Circle]]
, but other functions afterwards may modify it to be a List[Sequence[Circle]]
.
In order to determine which case we're in, Mypy would have to keep track of when our variables were declared, and ensure that nothing ever depends on treating the return value as a List[List[Circle]]
after the function returns (even though it is typed as such) before allowing us to use it as a return value.
(Note that treating it like a List[List[Circle]]
before the function returns shouldn't be a bad thing, since it is a List[List[Circle]]
at those points. Also if it was always treated like it was a List[Sequence[Circle]]
, then we could just type it as such with no problem. The question arises when something treats it like a List[List[Circle]]
, for example with circle_list_matrix[0].append(Circle())
, so we have to type it as a List[List[Circle]]
in order to do that operation, but then it's treated as a List[Sequence[Circle]]
every single time after the function returns.)
The bottom line is that Mypy doesn't do that sort of analysis. So, in order to let Mypy know that this is okay, we should just cast it.
In other words, we know that the return value will never be used as a List[List[Circle]]
again, so baz
should be written as:
def baz(circle_list_matrix: List[List[Circle]]) -> List[Sequence[Circle]]:
# works fine
return cast(List[Sequence[Circle]], circle_list_matrix)
where cast
is imported from typing
.
The same casting technique can be applied to bar
in the question code.