I have a base class that defines a class factory.
Then I have user defined classes which can extend the base class but which should use the class factory to create the class instance.
From a code perspective this works well and without issues, however I would like that the class factory is properly type hinted.
Unfortunately I can’t seem to make it work.
from typing import ParamSpec, TypeVar, Type
P = ParamSpec("P")
T = TypeVar("T")
class A:
def __init__(self, p1: int):
pass
@classmethod
def class_factory(cls: Type[T], *args: P.args, **kwargs: P.kwargs) -> T:
return cls(*args, **kwargs)
class B(A):
def __init__(self, p1: int, p2: str):
super().__init__(p1)
B.class_factory() # should be an error but is ok
B.class_factory('asdf', 1) # should be an error but is ok
If I a make a stand alone function this works well:
from typing import Generic, ParamSpec, TypeVar, Type
P = ParamSpec("P")
T = TypeVar("T")
class A(Generic[P]):
def __init__(self, p1: int):
pass
@classmethod
def class_factory(cls: Type[T], *args: P.args, **kwargs: P.kwargs) -> T:
return cls(*args, **kwargs)
class B(A[[int, str]]):
def __init__(self, p1: int, p2: str):
super().__init__(p1)
B.class_factory() # E: Too few arguments for "class_factory" of "A" [call-arg]
B.class_factory('asdf', 1) # E: Argument 1 to "class_factory" of "A" has incompatible type "str"; expected "int"
# Argument 2 to "class_factory" of "A" has incompatible type "int"; expected "str" [arg-type]
Thank you very much @tmk for your quick reply.
Is there any solution that doesn’t rely on defining A as a generic?
Because if I the user has to define it as a generic he can also just override the class factory which I think is easier especially for new programmers:
Well, the P has to appear somewhere else in addition to the *args and **kwargs annotation because otherwise the type checker doesn’t know what signature P is supposed to represent.
It seems you want to do something like this:
from typing import Callable, Self
class A:
@classmethod
def factory(cls: Callable[P, Self], *args: P.args, **kwargs: P.kwargs) -> Self: ...
where you can treat the class as a callable. But I don’t think that’s possible.
Perhaps you prefer this?
from typing import Generic, ParamSpec, Self
P = ParamSpec("P")
class FactoryMixin(Generic[P]):
@classmethod
def class_factory(cls, *args: P.args, **kwargs: P.kwargs) -> Self:
return cls(*args, **kwargs)
class A(FactoryMixin[[int]]):
def __init__(self, p1: int):
pass
class B(FactoryMixin[[int, str]]):
def __init__(self, p1: int, p2: str):
pass
B.class_factory()
B.class_factory('asdf', 1)
But this still has the problem that P is not inferred from the signature of __init__ so you have to specify it manually.
Yes - repeating the signature is tedious and error prone if it has to be done for 50+ classes on the user side.
The whole idea is to not having to repeat the signature which is not achieved with this solution.
I think the best approach for now is the create_class stand alone function, even if I think it’s not very elegant.
Do you think it’s worth opening an issue or is this a rather uncommon use case?