Enum classes and Self

The 4 major type checkers are handling this snippet very differently:

(playground links: Mypy, Pyright, Pyre; Pytype was run with -k -n)

from enum import Enum, auto
from typing import Self, final

class C: ...
class CEnum(C, Enum): ...
class CI(CEnum):
    A = auto()
    B = auto()

    @classmethod
    def return_member(cls) -> Self:
        # mypy     => Incompatible return value type (got "CI", expected "Self")
        # pyright  => Expression of type "Literal[CI.A]" is incompatible with return type "Self@CI"
        # pyre     => Expected `Variable[_Self_playground_input_CI__ (bound to CI)]` but got `CI`.
        # pytype   => fine
        # expected => fine
        return cls.A
@final
class FinalCI(CEnum):
    A = auto()
    B = auto()

    @classmethod
    def return_member(cls) -> Self:
        # mypy     => fine
        # pyright  => Expression of type "Literal[FinalCI.A]" is incompatible with return type "Self@FinalCI"
        # pyre     => Expected `Variable[_Self_playground_input_FinalCI__ (bound to FinalCI)]` but got `FinalCI`.
        # pytype   => fine
        # expected => fine
        return cls.A
# mypy     => Cannot extend enum with existing members: "CI"
# pyright  => Enum class "CI" is final and cannot be subclassed
# pyre     => fine
# pytype   => fine
# expected => error
class CIChild(CI):
    ...
@final
class D:

    @classmethod
    def return_member(cls) -> Self:
        # mypy     => fine
        # pyright  => Expression of type "D" is incompatible with return type "Self@D"
        # pyre     => Expected `Variable[_Self_playground_input_D__ (bound to D)]` but got `D`.
        # pytype   => fine
        # expected => fine
        return D()

Results:

  • Mypy: 3/4
  • Pyright: 1/4
  • Pyre: 0/4
  • Pytype: 3/4

According to the specs:

An Enum class with one or more defined members cannot be subclassed. They are implicitly “final”.

Perhaps a clarification like this could be added along with corresponding tests?

When used as the return type of an enum class’s method, Self means the same as the class itself:

class Shape(EnumWithNoMembers):
    SQUARE = 1
    CIRCLE = 2

    def m(self) -> Self:  # OK (there can be no subclasses of Shape)
        return Shape.SQUARE

    @classmethod
    def cm(cls) -> Self:  # OK (there can be no subclasses of Shape)
        return Shape.SQUARE
1 Like

I recommend against using Self in this case. The Self type is designed to support subclassing, and as you’ve pointed out, enums cannot be subclassed. All of the type checkers work fine if you switch to Shape.

2 Likes