Search code examples
pythonpython-3.xpycharmmypytyping

How to correctly override/overload typing of __call__


I need to clarify type, returning by unified factory. I have singleton factory in separate file like this:

from typing import Any, Type


class SingletonFactory:
    __slots__ = ('singleton_instance', )

    def __init__(self, singleton_class: Type[object], **singleton_init_params: Any):
        self.singleton_instance: object = singleton_class(**singleton_init_params)  # type: ignore[call-arg]  # noqa: E501

    def __call__(self) -> object:
        return self.singleton_instance

Then in other file.

Option 1:

from typing import Callable, overload

class Client:
   pass  # IRL have init params


client_factory: Callable[[], Client] = SingletonFactory(
    Client
)

client = client_factory()  # pyCharm see it as instance of object - expected Client

MyPy error:

error: Incompatible types in assignment (expression has type "SingletonFactory", variable has type "Callable[[], Client]")  [assignment]
note: "SingletonFactory.__call__" has type "Callable[[], object]"

Option 2:

from typing import Callable, overload

class Client:
   pass  # IRL have init params

@overload  # type: ignore[misc]
def client_factory() -> Client:
    ...


client_factory = SingletonFactory(
    Client
)

client = client_factory()  # pyCharm see it as instance of Client - what I expect

Works, but MyPy error same as above plus: error: Single overload definition, multiple required [misc].

Option 3:

from typing import Callable, overload

class Client:
   pass  # IRL have init params


class ClientFactory(SingletonFactory):

    @overload  # type: ignore[misc]
    def __call__() -> Client:
        ...


client_factory = ClientFactory(
    Client
)

client = client_factory()  # pyCharm see it as instance of Client - what I expect

One less MyPy error, but inheriting SingletonFactory only for overload makes code cumbersome.

Is there any way to fully satisfy MyPy in this situation?


Solution

  • You're looking for generics, to describe a relationship between the types of different variables in your code; one such implementation is demonstrated in the code below.

    from typing import Callable, Generic, ParamSpec, TypeVar
    
    P = ParamSpec("P")
    T = TypeVar("T")
    
    
    class SingletonFactory(Generic[T]):
        __slots__ = ('singleton_instance', )
    
        singleton_instance: T
    
        def __init__(
            self,
            singleton_class: Callable[P, T],
            *singleton_args: P.args,
            **singleton_kwargs: P.kwargs
        ):
            self.singleton_instance = singleton_class(*singleton_args, **singleton_kwargs)
    
        def __call__(self) -> T:
            return self.singleton_instance
    

    Now the return type of __call__ is based on the type of the first parameter to __init__:

    class Thing: pass
    
    
    t: Thing = SingletonFactory(Thing)()  # OK
    

    Alternatively, you can explicitly specify a factory of a particular type somewhere by providing the generic type:

    def my_func(factory: SingletonFactory[int]) -> int:
        return factory()
    

    It should be noted that, rather than using Type[T], Callable[P, T] has been used instead, with a ParamSpec (implemented in Python 3.10). When the program is it written this way, it allows the parameters to be checked too, based on the singleton_class:

    class OtherThing:
        def __init__(self, foo: int, bar: str):
            self.foo = foo
            self.bar = bar
    
    
    ot: OtherThing = SingletonFactory(OtherThing)()  # error: Missing positional arguments "foo", "bar" in call to "SingletonFactory"
    
    ot = SingletonFactory(OtherThing, 123, bar="baz")()  # OK
    

    ...and you no longer need type: ignore[call-arg].

    MyPy playground