Search code examples
pythonpython-typingpyright

Python list[T] not assignable to list[T | None]


I have a type T, a namedtuple actually:

from collections import namedtuple

T = namedtuple('T', ('a', 'b'))

I have a function that accepts a list[T | None] and a list:

def func(arg: list[T | None]):
    ...


l = [T(1, 2), T(2, 3)]
func(l)  # Pylance error

l is of type list[T].

When I pass a l to the function I get an error from Pylance, that a list[T] is incompatible with list[T | None] because T cannot be assigned to T | None.

Aside from manually specifying my list[T] is actually a list[T | None], what can I do to make this work without an error? Of course at runtime everything runs as expected.


Solution

  • The built-in mutable generic collection types such as list are all invariant in their type parameter. (see PEP 484)

    This means that given a type S that is a subtype of T, list[S] is not a subtype (nor a supertype) of list[T].

    You can simplify your error even further:

    def f(a: list[int | None]) -> None: ...
    
    b = [1, 2]
    f(b)
    

    int is obviously a subtype of int | None, but list[int] is not a subtype of list[int | None].

    For the code above, Mypy is so kind as to present additional info telling us exactly that:

    error: Argument 1 to "f" has incompatible type "List[int]"; expected "List[Optional[int]]"  [arg-type]
    note: "List" is invariant -- see https://mypy.readthedocs.io/en/stable/common_issues.html#variance
    note: Consider using "Sequence" instead, which is covariant
    

    Aside from the obvious solution you mentioned of explicitly annotating your list as list[T | None] beforehand, maybe you could follow the advice from Mypy and instead change the type accepted by your function to something less specific that still offers the protocols you need, but also happens to be covariant in its type parameter, e.g. collections.abc.Sequence:

    from collections import namedtuple
    from collections.abc import Sequence
    
    T = namedtuple('T', ('a', 'b'))
    
    
    def f(a: Sequence[T | None]) -> None:
        pass
    
    
    b = [T(1, 2), T(2, 3)]
    f(b)
    

    This should pass without errors.


    PS:

    You did not mention, what exactly your function is doing with that list, but the solution with an immutable type like Sequence will obviously not work, if you intend to mutate it inside the function.

    Mutablity really is the issue here and the reason why list is declared to be invariant in the first place. The Mypy docs offer a really nice explanation for this reasoning.

    So while it is technically possible to for example define your own generic protocol with a covariant type variable that emulates the methods you need in that function, I am not sure that would be a good idea. Better to reconsider what you actually need from that function and what would be safe to call it with.