3

To illustrate the problem, I'm defining a PositiveNumber class that is a subclass of a Number class. As Python doesn't support type casting, I'm defining the from_number as a convenience method I can use to cast one instance to another. This method is common to all subtypes of the Number class so I'm putting it into the parent class.

from __future__ import annotations

from typing import Any, Dict


class Number:
    def __init__(self, value: float, *args, **kwargs) -> None:
        self.value = value

    @classmethod
    def from_number(cls, other: Number) -> Number:
        return cls(**other.dict())

    def dict(self) -> Dict[str, Any]:
        return {
            "value": self.value
        }


class PositiveNumber(Number):
    def __init__(self, value: float, *args, **kwargs) -> None:
        if value <= 0:
            raise ValueError("Value must be a positive float value.")
        super().__init__(value)

    @property
    def is_positive(self) -> bool:
        return True
x = Number(1001) # this could be a positive number

# instead of writing this
y = PositiveNumber(value=x.value)

# I would like to be able to do this
y = PositiveNumber.from_number(x)

type(y)
# >> <class '__main__.PositiveNumber'>

As we can see, y is an instance of PositiveNumber as expected, but it's annotated as an instance of Number.

To avoid this, I would have to overwrite from_number in each subclass to correct the output annotation.

class PositiveNumber(Number):
    def __init__(self, value: float, *args, **kwargs) -> None:
        if value <= 0:
            raise ValueError("Value must be a positive float value.")
        super().__init__(value)

    @classmethod
    def from_number(cls, other: Number) -> PositiveNumber: #overridden
        return super().from_number(other)

    @property
    def is_positive(self) -> bool:
        return True

My thinking is that from_number should be an interface method instead, but then I will have to implement the same functionality in every single subtype, which will be tedious because the code will mostly be the same.

Is there a more elegant way of doing this or is this a completely bad design?

2
7

The concept that you need is called a MyType or SelfType in Type Theory. As you discovered, this is a very useful feature, with two major use cases (clone / copy / dup methods and factory / parse / deserialization methods), and I am always surprised that practically no mainstream type system has this feature. The only exceptions off the top of my head are TypeScript and Rust.

[Note: Scala has a concept named Self Type as well, but that is something different: it allows you to assign a type to this rather than to refer to the type of this. It also has Singleton Types which allow you to refer to a type only inhabited by a particular object, including something like this.type. But this is too restrictive: this refers to a type whose only instance is the current this, not to any instance which has the same type as this.]

With Python and MyPy, you are both in luck and out of luck: the good news is that Python has a Draft specification for Self Types in PEP 673:

from typing import Any, Dict, Self


class Number:
    @classmethod
    def from_number(cls, other: Number) -> Self:
        return cls(**other.dict())

The bad news is that it is still a Draft, slated for implementation in Python 3.11 which is currently scheduled for release in 2022-Q4. It is already implemented in Pyright and in a fork of MyPy, but it is not yet implemented in typing (but there is a Pull Request) nor in the official release of MyPy.

Which means that, at the moment, you have to either use Pyright (which, however, makes no sense because while your code will statically type check, it will then fail at runtime with a missing import) or the workaround documented in PEP 673:

from typing import Any, Dict, TypeVar


Self = TypeVar("Self", bound="Number")

class Number:
    @classmethod
    def from_number(cls: type[Self], other: Number) -> Self:
        return cls(**other.dict())
1
  • Thank you! This is really thorough and helpful.
    – Aresto
    Jan 9 at 15:15

Your Answer

By clicking “Post Your Answer”, you agree to our terms of service, privacy policy and cookie policy

Not the answer you're looking for? Browse other questions tagged or ask your own question.