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