Mypy and class inheritance

I’m writing an attrs class called SessionInfo that looks like this:

from attrs import define, field

@define(kw_only=True)
class ControllerInfo:
   controller_data: int = field(default=0)

@define(kw_only=True)
class SessionInfo:
    controllers: dict[str, ControllerInfo] = field(factory=dict)

ControllerInfo should serve as a base class for other classes in order to inherit the original attributes and be able to expand on those.

I’m trying to explain that to the type checker, but for example when doing:

@define
class MyInfo(ControllerInfo):
   other_controller_data: str

ctrls = {
    "my_controller": MyInfo()
    "my_other_controller": MyInfo()
}

session = SessionInfo(ctrls)

mypy returns the following error:

Argument "controllers" to "SessionInfo" has incompatible type "dict[str, MyInfo]"; expected "dict[str, ControllerInfo]"

In itself it’s not a too big of a problem because this is part of a plugin architecture and I know that at runtime it should not cause problems (unless the user specifically doesn’t inherit from ControllerInfo). At the same time it bothers me a bit in testing and I would like to address this.

I tried using dict[str, builtins.type[ControllerInfo] because I’m trying to specify that the values of the dictionary should be of type ControllerInfo and any of its descendants; but apparently that didn’t work, and now my understanding of the differences between builtins.type and typing.Type went out the window as well.

So the question is: how do I instruct mypy to understand that the values of Session.controllers can be descendants of ControllerInfo?

A secondary question: did I misunderstand the definitions of builtins.type and typing.Type? My understanding was:

  • builtins.type[Class] is used to specify objects that are of type Class and any of its descendants;
  • typing.Type[Class] is used to specify classes of type Class and any of its descendants.

EDIT: sorry for the multiple edits, lots of typos

dict is invariant, and I think Mypy is inferring the wrong (not actually wrong, but not what you’re expecting) type for ctrls. Try this:

ctrls: dict[str, ControllerInfo] = {
    "my_controller": MyInfo()
    "my_other_controller": MyInfo()
}

and see if it works.

@Tinche thanks, that did the trick. I still had to a call to cast somewhere else in the testing code because there’s another class that expects in its constructor an instance of MyInfo, i.e.

class Mock:
    def __init__(self, ctrl: MyInfo) -> None:
        self._ctrl = ctrl

And using you approach I have to do:

ctrl = controllers["ctrl1"]

mock = Mock(cast(MyInfo, ctrl))

It’s not ideal, but I can live with that in testing.

Another question: is there an alternative to dict that I can use to let the type checker understand my behavior?

There is no difference, typing.Type is a legacy name.