Copying signature of __init__ to a mixin class method

Hi all! I’m very green with the advanced use of the typing module, so please bear with me.

I’ve seen a lot of discussion around generics, ParamSpec, and how to copy signatures of functions to other functions, like those discussed here and here. However, it seems these solutions involve explicitly passing in the function to a decorator, or to create a TypeDict, which I don’t think will work for my use case. Maybe my use case is not a good one and should be handled in another way (I’m all ears), but let me explain what I’m trying to accomplish.

I have a class which inherits from pydantic’s BaseModel

from pydantic import BaseModel

class MyClass(BaseModel):
    field1: int
    field2: str

foo = MyClass(field1=..., field2=...)

When I go to type the last line, the type hinter (I’m using VS Code with the built-in pyright type checker), it correctly provides field1 and field2 when I go to create the class. I know this is some magic done in pydantic by replacing signatures (maybe only for runtime?) and with the use of the @typing.dataclass_transform decorator on BaseModel or its parent metaclass. I haven’t looked into the details of how the decorator works.

I am looking to build a mixin class which adds a “alternate constructor” for these classes. Example:

from pydantic import BaseModel

class MyMixin:
    @classmethod
    def alt_construct(cls, *args, **kwargs):
        ...
        return cls(*args, **kwargs)

class MyClass(MyMixin, BaseModel):
    field1: int
    field2: str

foo = MyClass.alt_constructor(...)

Right now, if I type the last line, the signature for alt_constructor is just *args: Any, **kwargs: Any. I am seeking a way that I can type hint these mixin class methods with the signature of the __init__ or __call__ functions of the class new class inheriting the mixin.

I came across this comment which sounds like they’re trying to doing what I want to achieve but within the same class.

I have tried to use a decorator approach, but that requires a syntax like:

def source_func(a: int, b: str) -> float: ...

@copy_params_from(source_func)
def target_func(*args, **kwargs): ...

The source_func where the parameters are being copied from has to be in scope. In the mixin, they are not in scope where the decorated function is written.

I have tried to write a Generic class with a ParamSpec, but so far none of them have resulted in the correct type hinting - all of them have been empty type hints (e.g. (...)) or just (*args: Any, **kwargs: Any) straight from the signature of target_func. The closest I have gotten is a solution using typing.cast, but this required the alternate constructor to be in the same class and have an explicitly defined __init__ function. Even then, it copied the self parameter in addition to the others.

Is there a way that I can type hint alt_constructor from my example MyMixin class in a generic way that the static type hint readers can understand, and which changes depending on which class it’s mixed into?

1 Like

Here’s an approach which takes advantage of

  • typing.Self, which is automatically transformed into a type variable with the current class (here, whatever you’re subclassing MyMixin with) as the upper bound, and
  • builtins.type[C], which must support the constructor signature (__init__ / __new__ / metaclass __call__) of a class C.

@dataclasses.dataclass is used in the following as a replacement of pydantic.BaseModel to demonstrate this in action in type-checkers (mypy playground, pyright playground):

from dataclasses import dataclass
from typing import ClassVar, Self, TYPE_CHECKING

class MyMixin:
    if TYPE_CHECKING:
        alt_construct: ClassVar[type[Self]]
    else:
        @classmethod
        def alt_construct(cls, *args, **kwargs):
            ...
            return cls(*args, **kwargs)

@dataclass
class MyClass(MyMixin):
    field1: int
    field2: str

foo = MyClass.alt_construct(
    "",           # error: got `str`, expected `int`
    field2=[""],  # error: got `list[str]`, expected `str`
)

Thank you, @xmw! That does work to get the parameters correct. In your example, right now the return type of alt_construct is None. Do you know of a way you could modify this so the return type of alt_construct is MyClass? It’s not absolutely crucial, but it would be nice to be fully correct. Thanks, again :slight_smile:

Do you know of a way you could modify this so the return type of alt_construct is MyClass ?

The return type is evaluated correctly by the type checker (both pyright and mypy). However, the language server (pylance) is displaying only the __init__ signature in this case. You could make an argument that it should display the full (combined) signature of the constructor, which would have a return type of MyClass. If you’d like, you can submit an enhancement request here for the pylance team.

Thanks, Eric! I appreciate the suggestion.

Based on how the alt_construct resource is defined inside the TYPE_CHECKING context, it’s posing as a MyClass object, right? So, its signature should just be the __init__ of that class, which is being shown. Forgive my ignorance, but how would the language server know that it should combine the signatures of the assigned value when TYPE_CHECKING is True and False? Or does the language server look at the entire context of the code, as opposed to pyright which only looks at those in the TYPE_CHECKING=True context?

Am I asking the wrong question? :sweat_smile: