pythonpython-typing

Type hint return type of abstract method to be any instance of parent class


How can I type hint that the return type of a method of some abstract class is some instance of this class?

My intuitive answer is that

@abstractmethod
def mymethod() -> typing.Self:
    pass

should be the correct way (as suggested in this post for example). But when I now subclass from this class, the return type is restricted to the type of the child class. What is the correct way to type hint here, so every subclass of the parent class is allowed?

Example code:

from abc import ABC, abstractmethod
from typing import Self, override
import random

class DiceResult(ABC):
    @abstractmethod
    def reroll(self) -> Self:
        pass

class NaturalTwenty(DiceResult):
    @override
    def reroll(self) -> DiceResult: # Type checkers report an error here
        return random.choice([NaturalTwenty(), NaturalOne()])

class NaturalOne(DiceResult):
    @override
    def reroll(self) -> DiceResult: # Type checkers report an error here
        return random.choice([NaturalTwenty(), NaturalOne()])

Mypy playground Basedpyright playground


Solution

  • @Schwern already explains why typing.Self does not work here in their answer. This is because typing.Self always type hints the current enclosing class. If it is the subclass of another class, the type hint refers to the subclass.

    To solve this, we want to use the parent class as type annotation, as in

    class DiceResult(ABC):
        @abstractmethod
        def reroll(self) -> DiceResult:
            pass
    

    This does not work in the current python version though, as it is a forward reference. The current solution is to use a string literal instead, as in

    class DiceResult(ABC):
        @abstractmethod
        def reroll(self) -> "DiceResult":
            pass
    

    In the future, the first version is intended to work as well, and is also already (python >= 3.7) available in __future__. It can be used as

    from __future__ import annotations
    
    class DiceResult(ABC):
        @abstractmethod
        def reroll(self) -> DiceResult:
            pass