user2799096
user2799096

Reputation: 181

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...

Upvotes: 16

Views: 17796

Answers (3)

unique2
unique2

Reputation: 2302

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*]'

Upvotes: 1

ogurets
ogurets

Reputation: 628

from __future__ import annotations
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(Generic[T]):
    def __init__(self) -> None:
        self.signal = Signal[T]()

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


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


class C:
    pass


def handle_c(c: C) -> None:
    print(c)


class B(A[B]):
    def __init__(self) -> None:
        super().__init__()
        self.signal.connect(handle_b)  # OK
        self.signal.connect(handle_c)  # incompatible type

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

Upvotes: 0

Martijn Pieters
Martijn Pieters

Reputation: 1121168

The type hint error is entirely correct. You created a Signal instance with A as the type, in the __init__ method of A:

self.signal = Signal[A]()

Passing in a subclass is fine, but all code interacting with that Signal instance now has to work for A instances only. handle_b() on the other handrequires an instance of B, and can't lower the requirement to A instead.

Drop the constraint:

self.signal = Signal()

or create an instance in each subclass with the correct type.

Upvotes: 0

Related Questions