How to properly hint a class factory with ParamSpec

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:

def create_class(f: Callable[P, T], *args: P.args, **kwargs: P.kwargs) -> T:
    return f(*args, **kwargs)


create_class(B, 'a', 'b') # <-- correctly identified as error

I tried annotating the class factory like the stand alone function but it doesn’t work.
Can anyone give me any hints?
Thank you for your help!

Here you go:

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]

By the way, for typing questions, you will probably usually get more help at python/typing · Discussions · GitHub instead of here.

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:

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)

    @classmethod
    def class_factory(cls,  p1: int, p2: str):
        return super().class_factory(p1, p2)

Thank you for your hint. If I don’t get any more help I’ll take a look there (but it doesn’t seem that way so far :wink: )

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?

Anyway - thank you very much for you help! :+1:

I’m pretty sure it would need a new PEP to get that feature. You can
pitch it in the Ideas category if you want.