How to combine a custom protocol with the Callable protocol?

Since typing.Callable corresponds to collections.abc.Callable, you can just define a Protocol that implements __call__:

class CallableWithAttrs(Protocol):
    attr1: str
    attr2: str

    def __call__(self, *args, **kwargs): pass

One can parameterise a Protocol by a Callable:

from typing import Callable, TypeVar, Protocol

C = TypeVar('C', bound=Callable)  # placeholder for any Callable


class CallableObj(Protocol[C]):   # Protocol is parameterised by Callable C ...
    attr1: str
    attr2: str

    __call__: C                   # ... which defines the signature of the protocol

This creates an intersection of the Protocol itself with an arbitrary Callable.


A function that takes any callable C can thus return CallableObj[C], a callable of the same signature with the desired attributes:

def decorator(func: C) -> CallableObj[C]: ...

MyPy properly recognizes both the signature and attributes:

def dummy(arg: str) -> int: ...

reveal_type(decorator(dummy))           # CallableObj[def (arg: builtins.str) -> builtins.int]'
reveal_type(decorator(dummy)('Hello'))  # int
reveal_type(decorator(dummy).attr1)     # str
decorator(dummy)(b'Fail')  # error: Argument 1 to "dummy" has incompatible type "bytes"; expected "str"
decorator(dummy).attr3     # error: "CallableObj[Callable[[str], int]]" has no attribute "attr3"; maybe "attr2"?