Return type Self: returning subclass

Can I use typing.Self to annotate that the method returns the class it belongs or its subclass?

Mypy reports an error here:

from typing import *


class A:
    def meth(self) -> Self:
        return B()  # Incompatible return value type (got "B", expected "Self")


class B(A):
    pass

When I replace Self with 'A', no error is reported. Should it really work like this or is it mypy issue? typing.Self here means A class, why doesn’t Self annotation approve subclasses of A but 'A' annotation does?

from typing import *


class A:
    def meth(self) -> 'A':
        return B()  # No typing error here


class B(A):
    pass

Self means an instance of the same class as self. In your example, if A.meth is annotated as returning Self, that means that for any subclass of A, the method should return that subclass if invoked on it. For example, given another subclass C of A, calling .meth() on an instance of C should return a C.

3 Likes

Mypy is working correctly here.

You can think of Self as a type variable with an upper bound of the containing class. Your first example is therefore equivalent to:

class A:
    def meth[S: A](self: S) -> S:
        return B()

Here’s how your first example can be modified to eliminate the type error.

class A:
    def meth(self) -> Self:
        return type(self)()

If you intend for the method to return A or any subtype of A, then you can specify that as you’ve done in your second example with a return type annotation of A.

To understand more about Self and how it is treated by type checkers, you may find this section of the typing spec useful.

3 Likes

I’m at risk of just restating previous replies here, but given the duplicate thread you opened, it’s not clear they were understood.

Annotating the return type as typing.Self here is correct, and provides exactly the constraint you want. The problem is that a hardcoded return of B() in A.meth implementation violates the constraint you claim to want! There could also be a class C(A): pass which does not override the method, and then calling C().meth() would return a B instance, which is not a subtype of C. Thus, you can’t hardcode some specific subclass in a super-class method, and expect it to satisfy a return type of Self.

2 Likes

How to correctly add type annotations in the following case:

I have a base configuration class:

class ConfigurationBase(ABC):
    def __init__(self, parsed_data: Any):
        ...  # Constructor from parsed data

    @classmethod
    @abstractmethod
    def parse(cls, data: dict) -> Self:
        ...

And multiple configuration types and versions (client and server configurations, format version 1, format version 2). Every configuration is subclass of ConfigurationBase, ServerConfigV1 is subclass of ServerConfiguration etc.

class ServerConfiguration(ConfigurationBase):
    @classmethod
    @override
    def parse(cls, data: dict) -> Self:
        if data['format_version'] == 1:
            return ServerConfigV1.parse(data)  # Incompatible return value type (got "ServerConfigV1", expected "Self")
        elif data['format_version'] == 2:
            return ServerConfigV2.parse(data)  # Incompatible return value type (got "ServerConfigV2", expected "Self")
        else:
            raise RuntimeError

class ServerConfigV1(ServerConfiguration):
    @classmethod
    @override
    def parse(cls, data: dict) -> Self:
        ...  # Parse code
        return cls(parsed_data)


class ServerConfigV2(ServerConfiguration):
    @classmethod
    @override
    def parse(cls, data: dict) -> Self:
        ...  # Another parse code
        return cls(parsed_data)


class ClientConfiguration(ConfigurationBase):
    @classmethod
    @override
    def parse(cls, data: dict) -> Self:
        ...  # Client parse code

Currently mypy shows type errors (see comments). How to fix typing here?

The problem here is that the return type of ServerConfiguartion.parse simply isn’t static; it’s determined by runtime data. Self in this context is meant to tie the return type to the fixed but unknown-as-yet type of cls, not an arbitrary and dynamically selectable type determined by a runtime value in data.

1 Like

You can make this a static method instead of a class method, and then remove use of Self for ServerConfiguration

This requires it be a static method in subclasses too, those subclasses can return the class and not Self and not violate subtyping rules

from __future__ import annotations
from typing import override


class ConfigurationBase:
    
    def __init__(self, parsed: object):
        ...
    
    @staticmethod
    def parse(data: dict) -> ConfigurationBase:
        ...


class ServerConfiguration(ConfigurationBase):
    @override
    @staticmethod
    def parse(data: dict) -> ServerConfiguration:
        if data['format_version'] == 1:
            return ServerConfigV1.parse(data)
        elif data['format_version'] == 2:
            return ServerConfigV2.parse(data)
        else:
            raise RuntimeError

class ServerConfigV1(ServerConfiguration):
    @override
    @staticmethod
    def parse(data: dict) -> ServerConfigV1:
        ...  # Parse code
        return ServerConfigV1(parsed_data)


class ServerConfigV2(ServerConfiguration):
    @override
    @staticmethod
    def parse(data: dict) -> ServerConfigV2:
        ...  # Another parse code
        return ServerConfigV2(parsed_data)


class ClientConfiguration(ConfigurationBase):
    @override
    @staticmethod
    def parse(data: dict) -> ClientConfiguration:
        ...  # Client parse code