Logo Questions Linux Laravel Mysql Ubuntu Git Menu
 

mypy: creating a type that accepts list of instances of subclasses

Tags:

python

mypy

Suppose I have a Child class that is a subclass of Parent class, and a function that accepts a list of instances of Parent subclasses:

from typing import List


class Parent:
    pass


class Child(Parent):
    pass


def func(objects: List[Parent]) -> None:
    print(objects)


children = [Child()]
func(children)

running mypy on this produces an error:

 error: Argument 1 to "func" has incompatible type "List[Child]"; expected "List[Parent]"

How do I create a type for this?

P.S. There's a way to fix this particular error with a Sequence type:

def func(objects: Sequence[Parent]) -> None:
    print(objects)

but this doesn't help in other similar cases. I need a List, not a Sequence.

like image 466
kurtgn Avatar asked Jan 27 '23 04:01

kurtgn


1 Answers

Passing in a list here is fundamentally not type-safe. For example, what if you do this?

def func(objects: List[Parent]) -> None:
    print(objects)
    objects.append(Parent())

children: List[Child] = [Child(), Child(), Child()]
func(children)
# Uh-oh! 'children' contains a Parent()!

If this were permitted to type check, your code would end up containing a bug.

To use type-jargon, List is intentionally designed to be an invariant type. That is, even though Child is a subclass of Parent, it is not the case that List[Child] is a subtype of List[Parent], or vice-versa. You can find more info about invariance here and here.

The most common alternative is to use Sequence instead, which is a read-only interface/protocol/whatever. And since Sequence is read-only, it's safe for it to be covariant: that is, Sequence[Child] is considered to be a valid subtype of Sequence[Parent].

Depending on what exactly you're doing, you may be able to use type variables instead. E.g. instead of saying "this function takes in a list of Parent", you say "this function takes in a list of any class which is Parent, or a subclass of Parent":

TParent = TypeVar('TParent', bound=Parent)

def func(objects: List[TParent]) -> List[TParent]:
    print(objects)

    # Would not typecheck: we can't assume 'objects' will be a List[Parent]
    objects.append(Parent())  

    return objects

Depending on what exactly you're doing, you could maaaaaaaaybe create a custom Protocol that defines a write-only list-like collection (or a custom data structure). And since your data structure would be write-only, you could make it contravariant -- that is, WriteOnlyThing[Parent] would be a subtype of WriteOnlyThing[Child]. You then make func accept WriteOnlyThing[Child] and could safely pass in instances of both WriteOnlyThing[Child] and WriteOnlyThing[Parent].

If neither approach works in your case, your only recourse is to either use # type: ignore to silence the error (not recommended), give up on type-checking the contents of the list and make the argument of type List[Any] (also not recommended), or figure out how to restructure your code so it's type-safe.

like image 143
Michael0x2a Avatar answered Jan 29 '23 19:01

Michael0x2a