1

I'm trying to use typehints and Mypy (also PyCharm) to enforce variance for containers, see, butchered, code below:

from typing import TypeVar, Generic

class T: ...
class M(T): ...
class B(M): ...

InMT = TypeVar('InMT', bound=M)
ContraMT = TypeVar('ContraMT', bound=M, contravariant=True)
CoMT = TypeVar('CoMT', bound=M, covariant=True)

class In(Generic[InMT]):
    x: InMT
class Contra(Generic[ContraMT]):
    x: ContraMT
class Co(Generic[CoMT]):
    x: CoMT

t = T()
m = M()
b = B()

m_in: In[M] = In()
m_contra: Contra[M] = Contra()
m_co: Co[M] = Co()

m_in.x = t  # mypy: Incompatible types in assignment (expression has type "T", variable has type "M").
m_in.x = m
m_in.x = b

m_contra.x = t # mypy: Incompatible types in assignment (expression has type "T", variable has type "M").
m_contra.x = m
m_contra.x = b

m_co.x = t # mypy: Incompatible types in assignment (expression has type "T", variable has type "M").
m_co.x = m
m_co.x = b

Mypy finds some problems, see comments in above code, and PyCharm finds none! However I think Mypy misses a number of problems, incorrectly reports a problem, and gives misleading error messages:

  1. Error message for m_in.x = t is wrong since the variable is of type InMT not M.

  2. m_in.x = b should be an error because a B is not an InMT (only an M is).

  3. m_contra.x = t should not be an error because a T is a ContraMT.

  4. m_contra.x = b should be an error because a B is not an ContraMT (only an M or a T are).

  5. As point 1 above, variable is of type CoMT not M.

What am I doing wrong; or am I misunderstanding what Mypy is meant to do?

bad_coder
  • 11,289
  • 20
  • 44
  • 72
Howard Lovatt
  • 968
  • 1
  • 8
  • 15

1 Answers1

1

I'm using PyCharm 2021.3.2 (latest version at time of writing) and I'm not seeing invariance, contravariance, and covariance type checking working correctly. It seems to (incorrectly) assume covariance all the time. Even though Pycharm claims support.

PEP 484 code below with the covariance flag removed does not flag an error which I believe it should as:

By default generic types are considered invariant in all type variables, which means that values for variables annotated with types like List[Employee] must exactly match the type annotation -- no subclasses or superclasses of the type parameter (in this example Employee) are allowed.

from typing import TypeVar, Generic, Iterable, Iterator

T = TypeVar('T', covariant=True)

class ImmutableList(Generic[T]):
    def __init__(self, items: Iterable[T]) -> None: ...
    def __iter__(self) -> Iterator[T]: ...
    ...

class Employee: ...

class Manager(Employee): ...

def dump_employees(emps: ImmutableList[Employee]) -> None:
    for emp in emps:
        ...

mgrs = ImmutableList([Manager()])  # type: ImmutableList[Manager]
dump_employees(mgrs)  # Should be caught by type-checker

Its also not caught with: mgrs: ImmutableList[Manager] = ImmutableList([Manager()])

Nor is it caught for a built-in list:

def dump_more_employees(employees: list[Employee]) -> None:
    for employee in employees:
        ...


managers: list[Manager] = list([Manager()])
dump_more_employees(managers)  # Should be caught by type-checker

When it should be:

Consider a class Employee with a subclass Manager. Now suppose we have a function with an argument annotated with List[Employee]. Should we be allowed to call this function with a variable of type List[Manager] as its argument? Many people would answer "yes, of course" without even considering the consequences. But unless we know more about the function, a type checker should reject such a call: the function might append an Employee instance to the list, which would violate the variable's type in the caller.

I can't speak to MyPy but I hope this was useful.

Robin Carter
  • 139
  • 9