Logo Questions Linux Laravel Mysql Ubuntu Git Menu
 

How to have inherited type hints in python?

So my problem is That when I have a class of type A that does things and I use those functions as a subclass(B) they are still typed for class A and do not accept my class B object as arguments or as function signature.

My problem simplified:

from typing import TypeVar, Generic, Callable

T = TypeVar('T')


class Signal(Generic[T]):
    def connect(self, connector: Callable[[T], None]) -> None:
        pass

    def emit(self, payload: T):
        pass


class A:
    def __init__(self) -> None:
        self.signal = Signal[A]()

    def do(self) -> None:
        self.signal.emit(self)

def handle_b(b: "B") -> None:
    print(b.something)

class B(A):
    def __init__(self) -> None:
        super().__init__()
        self.signal.connect(handle_b)

    @property
    def something(self) -> int:
        return 42

I can provide the complete signal class as well but that just distracts from the problem. This leaves me with one error in mypy:

error: Argument 1 to "connect" of "Signal" has incompatible type Callable[[B], None]; expected Callable[[A], None]

Since the signal handling is implemented in A the subclass B can't expect B type objects to be returned even though it clearly should be fine...

like image 706
user2799096 Avatar asked Dec 19 '17 22:12

user2799096


1 Answers

The connector passed to Signal[A] is of type Callable[[A], None], which means it has to promise to be able to handle any instance of A (or any of it's sub-classes). handle_b cannot fulfill this promise, since it only works for instances of B, it therefore cannot be used as a connector for a signal of type Signal[A].

Presumably, the connector of the signal of any instance of B will only ever be asked to handle an instance of B, it therefore doesn't need to be of type Signal[A], but Signal[B] would be sufficient. This means the type of signal is not fixed, but varies for different sub-classes of A, this means A needs to be generic.

The answer by ogurets correctly makes A generic, however there is no a problem with do, since it's unclear whether self is of type expected by self.signal.emit. We can promise that these types will always match by annotating self with the same type variable used for Signal. By using a new type variable _A which is bound by A, we tell mypy that self will always be a subtype of A and therefore has a property signal.

from __future__ import annotations

from collections.abc import Callable
from typing import TypeVar, Generic

T = TypeVar('T')


class Signal(Generic[T]):
    def connect(self, connector: Callable[[T], None]) -> None:
        pass

    def emit(self, payload: T):
        print(payload)

_A = TypeVar('_A', bound='A')

class A(Generic[_A]):
    signal: Signal[_A]

    def __init__(self) -> None:
        self.signal = Signal[_A]()

    def do(self) -> None:
        self.signal.emit(self)

def handle_b(b: "B") -> None:
    print(b.something)

class B(A['B']):
    def __init__(self) -> None:
        super().__init__()
        self.signal.connect(handle_b)

    @property
    def something(self) -> int:
        return 42

b = B()
reveal_type(b.signal) # Revealed type is '...Signal[...B*]'
like image 58
unique2 Avatar answered Sep 25 '22 16:09

unique2