Type check fails when instance is assigned, but not when passed directly to method

Here’s an MRE which has a generic Msg class that BaseHandler subclasses handle:

from abc import ABC
from dataclasses import dataclass
from typing import Generic, TypeVar

T = TypeVar('T')

@dataclass
class Msg(Generic[T]):
    data: T

class BaseHandler(Generic[T], ABC):
    def handle(self, msg: Msg[T]):
        ...

Add some classes to use in Msg:

class Foo:
    foo: bool

class Bar:
    bar: float

Add a handler which can handle both classes:

FooBar = Foo | Bar

class FooBarHandler(BaseHandler[FooBar]):
    def handle(self, msg: Msg[FooBar]):
        match (data := msg.data):
            case Foo():
                print('foo', data.foo)
            case Bar():
                print('foo', data.bar)

foobar_handler = FooBarHandler()
foobar_handler.handle(Msg(data=Foo()))
foobar_handler.handle(Msg(data=Bar()))

That all type checks :white_check_mark:


Now the problem: If I extract Msg to a variable:

msg = Msg(data=Foo())
foobar_handler.handle(msg)

Pyright:

error: Argument of type "Msg[Foo]" cannot be assigned to parameter "msg" of type "Msg[FooBar]" in function "handle"
    "Msg[Foo]" is incompatible with "Msg[FooBar]"
      Type parameter "T@Msg" is invariant, but "Foo" is not the same as "FooBar" (reportGeneralTypeIssues)

Mypy:

error: Argument 1 to "handle" of "FooBarHandler" has incompatible type "Msg[Foo]"; expected "Msg[Foo | Bar]"  [arg-type]

I can ‘workaround’ it by explicitly instantiating Msg with FooBar:

msg = Msg[FooBar](data=Foo())

But I feel like there should be a better way to let Python know that a Msg[Foo] is covariant with Msg[FooBar] in handle?

The Msg type has a writable data property. Therefore in general it needs to be invariant rather than covariant.