Having trouble getting mypy to recognize that variable implements a protocol

The following code generates an error when run with mypy --strict

from typing import Protocol

class Proto(Protocol):
  id: int
  name: str
  def work(self, hours: float) -> str: ...  

class RoleA:
  def work(self, hours: float) -> str:
    return f"A {hours}"

class RoleB:
  def work(self, hours: float) -> str:
    return f"B {hours}"

class X:
  def __init__(self, id: int, name: str) -> None:
    self.id = id
    self.name = name

class Y(X, RoleA):
  def __init__(self, id: int, name: str) -> None:
    super().__init__(id, name)

class Z(X, RoleB):
  def __init__(self, id: int, name: str) -> None:
    super().__init__(id, name)

def track(items: list[Proto], hours: float) -> None:
  for item in items:
    result = item.work(hours)


if __name__ == "__main__":
  y = Y(id = 4, name = "Jane Doe")
  z = Z(id = 3, name = "Kevin Bacon")
  items =  [y, z]
  track(items, 40)

The error is as follows:

program.py:38: error: Argument 1 to "track" has incompatible type "List[X]"; expected "List[Proto]"

I don't understand why this occurs, as far as I can tell Y and Z both implement the Proto protocol, so why isn't mypy able to deduce that they are valid arguments to the track function?


Solution 1:

Every variable gets a specific type bound to it when it's declared. For non-generic types with no annotation, it's simply the type of the object on the right side of the assignment, and this is almost always the right thing (unless you plan to reassign it to a different type later, in which case you need to declare it as a union beforehand so that it can accept either type).

When you don't explicitly declare the type parameter for a generic type, like a list or other collection:

items = [y, z]

mypy makes a best guess at it. In this case it's List[X], because X is the most obvious common type of y and z. The vast majority of the time the inferred type is the one you want, but sometimes it's either too broad or too narrow, in which case you can specify one explicitly:

items: list[Proto] = [y, z]  # or typing.List[Proto] for python <3.10

This will fix the error on your track call.

In general I don't think mypy will infer a Protocol type as the common type of multiple objects (since a given object might implement any number of protocols), so you'll need to declare it explicitly one way or another.