Search code examples
pythoninheritancesubclasstype-hinting

Type hint of Base class method based on Subclass


When creating a base class with a request method that will be exactly the same for all subclasses. There does not seem to be an easy way of having separate type-hints based on which subclass made the request.

The example below is how I currently am solving this issue, but it seems as if there is a better way.

class Response(TypeDict): ...

class FooResponse(Response): ...

class BarResponse(Response): ...

class Request:

    @classmethod
    def make_request(cls, args: Any) -> Response:
        # This will return a dict response based on cls.TYPE (if cls.TYPE == Foo, FooResponse will be returned)
        return execute_db_query(cls, args)


class FooRequest(Request):

    @classmethod
    def make_request(cls, args: Any) -> FooResponse:
        return FooResponse(**super().make_request(cls, args))


class BarRequest(Request):

    @classmethod
    def make_request(cls, args: Any) -> BarResponse:
        return BarResponse(**super().make_request(cls, args))

Is there a better way of doing this, either by specifying Response type on the subclass or by just overriding the method signature and no the functionality?

Something like (I know this does not work):

class FooRequest(Request):
    @classmethod
    def make_request(...) -> FooResponse: ...

I would assume that using something like Generic[T] and TypeVar() could be a way to go?


Solution

  • Perhaps typing.overload is what you seek.

    from __future__ import annotations
    from typing import overload
    
    class Request:
        @overload
        @classmethod
        def make_request(cls: FooRequest, args: Any) -> FooResponse: ...
        @overload
        @classmethod
        def make_request(cls: BarRequest, args: Any) -> BarResponse: ...
        
        def make_request(cls, args):
            return actual_implementation(cls, args)
    

    Now your FooRequest and BarRequest no longer need to supply their implementation of (or type hints for) make_request.


    If on the other hand your only goal is to type hint the IDE, you could:

    from __future__ import annotations
    from typing import TypeVar
    
    
    class Response: ...
    class FooResponse(Response): ...
    class BarResponse(Response): ...
    
    T = TypeVar('T', bound=Response, covariant=True)
    
    class Request:
        @classmethod
        def make_request(cls: Request, args: Any) -> T: 
              return actual_implementation(cls, args
    
    
    class FooRequest(Request):
        @classmethod
        def make_request(cls: FooRequest, args: Any) -> FooResponse:
              return super(FooRequest, cls).make_request(args)
    

    This would make the IDE accept

    fooresponse: FooResponse = FooRequest.make_request(None)
    

    but reject

    fooresponse: FooResponse = Request.make_request(None)