Search code examples
pythontype-hintingmypy

mypy not recognizing variables inherited from superclass


I've got the following class defined:

class BackgroundThread(threading.Thread):
    def __init__(self, *args: typing.Any, **kwargs: typing.Any) -> None:
        super().__init__(*args, **kwargs)
        self._quit = threading.Event()

    def run(self) -> None:
        while not self._quit.wait(timeout=0.0):
            self._target(*self._args, **self._kwargs)

    def __enter__(self) -> None:
        self.start()
        return None

    def __exit__(self, *args: typing.Any) -> None:
        self._quit.set()
        self.join()

The code works exactly as expected. However, mypy complains that

"BackgroundThread" has no attribute "_target"
"BackgroundThread" has no attribute "_args"
"BackgroundThread" has no attribute "_kwargs"

I can get around this by inserting

class BackgroundThread(threading.Thread):
    _target: typing.Callable
    _args: typing.Tuple
    _kwargs: typing.Dict[str, typing.Any]

However, this seems hacky. Why doesn't mypy recognize these variables which come from the parent class? Is it because they begin with underscores?


Solution

  • This is the definition of Thread in standard stub. Underscore attributes are not defined, only dunder are. You can ask/contribute to typeshed on Github to add missing underscore attributes. It is not forbidden to add them, but usually them are not listed in typeshed stubs.

    class Thread:
        name: str
        ident: Optional[int]
        daemon: bool
        if sys.version_info >= (3,):
            def __init__(
                self,
                group: None = ...,
                target: Optional[Callable[..., Any]] = ...,
                name: Optional[str] = ...,
                args: Iterable[Any] = ...,
                kwargs: Optional[Mapping[str, Any]] = ...,
                *,
                daemon: Optional[bool] = ...,
            ) -> None: ...
        else:
            def __init__(
                self,
                group: None = ...,
                target: Optional[Callable[..., Any]] = ...,
                name: Optional[Text] = ...,
                args: Iterable[Any] = ...,
                kwargs: Optional[Mapping[Text, Any]] = ...,
            ) -> None: ...
        def start(self) -> None: ...
        def run(self) -> None: ...
        def join(self, timeout: Optional[float] = ...) -> None: ...
        def getName(self) -> str: ...
        def setName(self, name: Text) -> None: ...
        if sys.version_info >= (3, 8):
            @property
            def native_id(self) -> Optional[int]: ...  # only available on some platforms
        def is_alive(self) -> bool: ...
        if sys.version_info < (3, 9):
            def isAlive(self) -> bool: ...
        def isDaemon(self) -> bool: ...
        def setDaemon(self, daemonic: bool) -> None: ...