10

Python 3.6.5 and mypy 0.600

I wrote the code:

from typing import List


class Animal():
    pass


class Dog(Animal):
    def __init__(self) -> None:
        super()

    def bark(self) -> None:
        pass


class Cat(Animal):
    def __init__(self) -> None:
        super()

    def meow(self) -> None:
        pass


arr1: List[Dog] = [Dog(), Dog()]
arr2: List[Animal] = [Dog(), Dog()]

# error: Incompatible types in assignment (expression has type "List[Dog]", variable has type "List[Animal]")
arr3: List[Animal] = arr1

I don't understand, why I have an error 'Incompatible types in assignment ' with a variable 'arr3'. Dog is a class which inherits from a Animal. For example, I don't have an error with variable 'arr2'.

Martijn Pieters
  • 1,048,767
  • 296
  • 4,058
  • 3,343
user3517175
  • 363
  • 2
  • 14

2 Answers2

9

Imagine that this would be possible:

arr3: List[Animal] = arr1

Now you think you have list of animals, but this is actually a list of dogs (note that arr3 is not a copy of arr1, they are the same list).

And because you think this is the list of animals you can add a Cat to it.

However, because this is actually list of dogs, you cannot add a Cat to it. Otherwise you will fail on AttributeError after trying to use dog-specific attribute.

More generally, list is invariant - List[Animal] cannot be assigned to List[Dog] (because it can already contain cats) and List[Dog] cannot be assigned to List[Animal] (because you can add cat later)


This might not be obvious in Python, but you can make simple test:

arr3: List[Animal] = arr1 
arr3.append(Cat())
for dog in arr1:
    print(dog.bark())

Mypy does not allow this because this assignment might break your code logic

awesoon
  • 32,469
  • 11
  • 74
  • 99
  • What's wrong with adding cat to `List[Animal]`? This was an issue with mypy that is now resolved in Python-3.7 – Mazdak May 12 '18 at 11:31
  • There is nothing wrong with adding cat to `List[Animal]`. There is problem when you are adding cat to `List[Dog]`. And because `arr1` is still list of dogs, it wont become list of animals after assigning to variable with type of `List[Animal]` – awesoon May 12 '18 at 11:33
  • Yes, but I don't see how this can be related to the types? what OP is trying to do is to add `List[Dog]` to `List[Animal]` it's a sensible hierarchy. Specially when they're dog inherits from animal. – Mazdak May 12 '18 at 11:38
  • 2
    Not sure why this was downvoted earlier: this answer is absolutely correct. For OP: if you'd like more details, mypy's docs has a [section on invariance vs covariance](http://mypy.readthedocs.io/en/latest/common_issues.html#invariance-vs-covariance). – Michael0x2a May 12 '18 at 11:39
  • 3
    Animal -> Dog is a hierarchy, but `List[Animal]` -> `List[Dog]` is not. – awesoon May 12 '18 at 11:39
  • @Kasramvd: note that OP's code isn't adding `List[Dog]` to `List[Animal]`: it's replacing the list outright. – Michael0x2a May 12 '18 at 11:40
  • @Michael0x2a Yep, now I see what's happening here. However the problem here is that it's replacing the list outright instead of reassigning the elements. And this is not a correct behavior when you're using typing. That's why they resolve it in Python-3.7. – Mazdak May 12 '18 at 11:47
  • @Kasramvd: I'm not really sure what you're referring to -- I pay pretty close attention to changes in mypy and the typing ecosystem as a whole and to my knowledge the semantics of lists, invariance, and covariance haven't been changed during at least the past 2 years. Doing `animals = dogs` has always been illegal; doing `animals.extend(dogs)` has always worked. – Michael0x2a May 12 '18 at 11:54
  • @Michael0x2a Well `arr3: List[Animal] = arr1` works fine in Python-3.7 and returns `[<__main__.Dog at 0x7f8e9b05fd68>, <__main__.Dog at 0x7f8e9b05fbe0>]` as expected. – Mazdak May 12 '18 at 12:10
  • @Kasramvd: That's because types are ignored at runtime. This has nothing to do with Python 3.7: all versions of Python 3 (starting from 3.0) will run OP's code (assuming the `typing` module backport is installed when necessary). However, type checking tools like mypy will reject OP's code regardless of what version of Python he or she is trying to run. For more details see [this post](https://stackoverflow.com/q/41356784/646543). – Michael0x2a May 12 '18 at 12:21
  • @Michael0x2a ah yes, you're right. It seems that I've mixed the behavior of Mypy with the built-in typing module. Seems kinda obscure now. It's too much abstractions quilted together here. – Mazdak May 12 '18 at 12:34
6

You can try using Sequence[Animal], which is covariant.

List[T] is invariant; it will only handle items of exactly type T. This means List[Dog] is not a subtype of List[Animal]. This is because of what @awesoon mentioned, which is that it prevents you from accidentally adding items which are incompatible with T:

# this won't compile:

dogs : List[Dog] = [dog1, dog2]
animals : List[Animal] = dogs # compiler error: List is invariant

# if the compiler allowed the previous line,
# then `dogs` would be [dog1, dog2, cat] after the next line
animals.push(cat1) 

On the other hand, Sequence[T] is covariant with T, which means that a Sequence[Dogs] is a subtype of Sequence[Animals]. This is allowed because a Sequence does not have "insert" methods, so you can never accidentally sneak a Cat in a Sequence[Dog]:

dogs : List[Dog] = [dog1, dog2]
animals: Sequence[Animals] = dogs # this is fair game for the compiler
animals.push(cat1) # compiler error: Sequence has no method push
# since Sequences can't add new items, you can't
# accidentally put a cat inside a list of dogs =) 
user986730
  • 1,226
  • 1
  • 13
  • 12