I'm trying to wrap the signal class of blinker with one that enforces typing so that the arguments to send and connect get type-checked for each specific signal. eg if I have a signal user_update which expects sender to be an instance of User and have exactly two kwargs: time: int, audit: str, I can sub-class Signal to enforce that like so: class UserUpdateSignal(Signal): class Receiver(Protocol): def __call__(sender: User, /, time: int, audit: str): ... def send(sender: User, /, time: int, audit: str): # super call def connect(receiver: Receiver): # super call which results in the desired behavior when type-checking: user_update.send(user, time=34, audit="user_initiated") # OK @user_update.connect # OK def receiver(sender: User, /, time: int, audit: str): ... user_update.send("sender") # typing error - signature mismatch @user_update.connect # typing error - signature mismatch def receiver(sender: str): ... The issues with this approach are: it's very verbose, for a few dozen signals I'd have hundreds of lines of code it doesn't actually tie the type of the send signature to that of the connect signature - they can be updated independently, type-checking would pass, but the code would crash when run The ideal approach would apply a signature defined once to both send and connect - probably through generics. I've tried a few approaches so far: Positional Args Only with ParamSpec I can achieve the desired behavior using only class TypedSignal(Generic[P], Signal): def send(self, *args: P.args, **kwargs: P.kwargs): super().send(*args, **kwargs) def connect(self, receiver: Callable[P, None]): return super().connect(receiver=receiver) user_update = TypedSignal[[User, str]]() This type-checks positional args correctly but has no support for kwargs due to the limitations of Callable. I need kwargs support since blinker uses kwargs for every arg past sender. Other Attempts Using TypeVar and TypeVarTuple I can achieve type-hinting for the sender arg pretty simply using generics: T = TypeVar("T") class TypedSignal(Generic[T], Signal): def send(self, sender: Type[T], **kwargs): super(TypedSignal, self).send(sender) def connect(self, receiver: Callable[[Type[T], ...], None]) -> Callable: return super(TypedSignal, self).connect(receiver) # used as my_signal = TypedSignal[MyClass]() what gets tricky is when I want to add type-checking for the kwargs. The approach I've been attempting to get working is using a variadic generic and Unpack like so: T = TypeVar("T") KW = TypeVarTuple("KW") class TypedSignal(Generic[T, Unpack[KW]], Signal): def send(self, sender: Type[T], **kwargs: Unpack[Type[KW]]): super(TypedSignal, self).send(sender) def connect(self, receiver: Callable[[Type[T], Unpack[Type[KW]]], None]) -> Callable: return super(TypedSignal, self).connect(receiver) but mypy complains: error: Unpack item in ** argument must be a TypedDict which seems odd because this error gets thrown even with no usage of the generic, let alone when a TypedDict is passed. Using ParamSpec and Protocol P = ParamSpec("P") class TypedSignal(Generic[P], Signal): def send(self, *args: P.args, **kwargs: P.kwargs) -> None: super().send(*args, **kwargs) def connect(self, receiver: Callable[P, None]): return super().connect(receiver=receiver) class Receiver(Protocol): def __call__(self, sender: MyClass) -> None: pass update = TypedSignal[Receiver]() @update.connect def my_func(sender: MyClass) -> None: pass update.send(MyClass()) but mypy seems to wrap the protocol, so it expects a function that takes the protocol, giving the following errors: error: Argument 1 to "connect" of "TypedSignal" has incompatible type "Callable[[MyClass], None]"; expected "Callable[[Receiver], None]" [arg-type] error: Argument 1 to "send" of "TypedSignal" has incompatible type "MyClass"; expected "Receiver" [arg-type] Summary Is there a simpler way to do this? Is this possible with current python typing? mypy version is 1.9.0 - tried with earlier versions and it crashed completely. Continue reading...