Skip to content

Latest commit

 

History

History
1900 lines (1347 loc) · 42.3 KB

File metadata and controls

1900 lines (1347 loc) · 42.3 KB

Enums

Basic

from enum import Enum
from typing import Literal

class Color(Enum):
    RED = 1
    GREEN = 2
    BLUE = 3

reveal_type(Color.RED)  # revealed: Literal[Color.RED]
reveal_type(Color.RED.name)  # revealed: Literal["RED"]
reveal_type(Color.RED.value)  # revealed: Literal[1]

# TODO: Could be `Literal[Color.RED]` to be more precise
reveal_type(Color["RED"])  # revealed: Color
reveal_type(Color(1))  # revealed: Color

reveal_type(Color.RED in Color)  # revealed: bool

Enum members

Basic

Simple enums with integer or string values:

from enum import Enum
from ty_extensions import enum_members

class ColorInt(Enum):
    RED = 1
    GREEN = 2
    BLUE = 3

# revealed: tuple[Literal["RED"], Literal["GREEN"], Literal["BLUE"]]
reveal_type(enum_members(ColorInt))

class ColorStr(Enum):
    RED = "red"
    GREEN = "green"
    BLUE = "blue"

# revealed: tuple[Literal["RED"], Literal["GREEN"], Literal["BLUE"]]
reveal_type(enum_members(ColorStr))

When deriving from IntEnum

from enum import IntEnum
from ty_extensions import enum_members

class ColorInt(IntEnum):
    RED = 1
    GREEN = 2
    BLUE = 3

# revealed: tuple[Literal["RED"], Literal["GREEN"], Literal["BLUE"]]
reveal_type(enum_members(ColorInt))

Annotated assignments with values are still members

If an enum attribute has both an annotation and a value, it is still an enum member at runtime, even though the annotation is invalid:

from enum import Enum
from ty_extensions import enum_members

class Answer(Enum):
    YES = 1
    NO = 2

    annotated_member: str = "some value"  # error: [invalid-enum-member-annotation]

# revealed: tuple[Literal["YES"], Literal["NO"], Literal["annotated_member"]]
reveal_type(enum_members(Answer))
reveal_type(Answer.annotated_member)  # revealed: Literal[Answer.annotated_member]
reveal_type(Answer.YES.annotated_member)  # revealed: Literal[Answer.annotated_member]

Enum members are allowed to be marked Final (without a type), even if unnecessary:

from enum import Enum
from typing import Final
from ty_extensions import enum_members

class Answer(Enum):
    YES: Final = 1
    NO: Final = 2

# revealed: tuple[Literal["YES"], Literal["NO"]]
reveal_type(enum_members(Answer))

Annotated enum members

The typing spec states that enum members should not have explicit type annotations. Type checkers should report an error for annotated enum members because the annotation is misleading — the actual type of an enum member is the enum class itself, not the annotated type.

[environment]
python-version = "3.11"
from enum import Enum, IntEnum, StrEnum, member
from typing import Callable, Final

class Pet(Enum):
    CAT = 1
    DOG: int = 2  # error: [invalid-enum-member-annotation] "Type annotation on enum member `DOG` is not allowed"
    BIRD: str = "bird"  # error: [invalid-enum-member-annotation]

Bare Final annotations are allowed (they don't specify a type):

class Pet2(Enum):
    CAT: Final = 1  # OK
    DOG: Final = 2  # OK

But Final with a type argument is not allowed:

class Pet3(Enum):
    CAT: Final[int] = 1  # error: [invalid-enum-member-annotation]
    DOG: Final[str] = "woof"  # error: [invalid-enum-member-annotation]

enum.member used as value wrapper is the standard way to declare members explicitly:

class Pet4(Enum):
    CAT = member(1)  # OK

Dunder and private names are not enum members, so they don't trigger the diagnostic:

class Pet5(Enum):
    CAT = 1
    __private: int = 2  # OK: dunder/private names are never members
    __module__: str = "my_module"  # OK

Pure declarations (annotations without values) are non-members and are fine:

class Pet6(Enum):
    CAT = 1
    species: str  # OK: no value, so this is a non-member declaration

reveal_type(Pet6.species)  # revealed: str
reveal_type(Pet6.CAT.species)  # revealed: str

Pure declarations in stubs

In stubs, these should still be treated as non-member attributes rather than enum members:

from enum import Enum

class Pet6Stub(Enum):
    species: str

    CAT = ...
    DOG = ...

reveal_type(Pet6Stub.species)  # revealed: str

Callable values and subclasses

Callable values are never enum members at runtime, so annotating them is fine:

[environment]
python-version = "3.11"
from enum import Enum, IntEnum, StrEnum
from typing import Callable

def identity(x: int) -> int:
    return x

class Pet7(Enum):
    CAT = 1
    declared_callable: Callable[[int], int] = identity  # OK: callables are never members

The check also works for subclasses of Enum:

class Status(IntEnum):
    OK: int = 200  # error: [invalid-enum-member-annotation]
    NOT_FOUND = 404  # OK

class Color(StrEnum):
    RED: str = "red"  # error: [invalid-enum-member-annotation]
    GREEN = "green"  # OK

Special sunder names like _value_ and _ignore_ are not flagged:

class Pet8(Enum):
    _value_: int = 0  # OK: `_value_` is a special enum name
    _ignore_: str = "TEMP"  # OK: `_ignore_` is a special enum name
    CAT = 1

Names listed in _ignore_ are not members, so annotating them is fine:

class Pet9(Enum):
    _ignore_ = "A B"
    A: int = 42  # OK: `A` is listed in `_ignore_`
    B: str = "hello"  # OK: `B` is listed in `_ignore_`
    C: int = 3  # error: [invalid-enum-member-annotation]

Unreachable declarations do not change membership

Statically unreachable declarations should be ignored when deciding whether a name is an enum member:

from enum import Enum
from ty_extensions import enum_members

class Pet10(Enum):
    if False:
        CAT: int

    CAT = 1
    DOG = 2

# revealed: tuple[Literal["CAT"], Literal["DOG"]]
reveal_type(enum_members(Pet10))
reveal_type(Pet10.CAT)  # revealed: Literal[Pet10.CAT]
reveal_type(Pet10.DOG)  # revealed: Literal[Pet10.DOG]

Declared _value_ annotation

If a _value_ annotation is defined on an Enum class, all enum member values must be compatible with the declared type:

from enum import Enum

class Color(Enum):
    _value_: int
    RED = 1
    GREEN = "green"  # error: [invalid-assignment]
    BLUE = ...
    YELLOW = None  # error: [invalid-assignment]
    PURPLE = []  # error: [invalid-assignment]

When _value_ is annotated, .value and ._value_ are inferred as the declared type:

from enum import Enum
from typing import Final

class Color2(Enum):
    _value_: int
    RED = 1
    GREEN = 2

reveal_type(Color2.RED.value)  # revealed: int
reveal_type(Color2.RED._value_)  # revealed: int

class WantsInt(Enum):
    _value_: int
    OK: Final = 1
    BAD: Final = "oops"  # error: [invalid-assignment]

_value_ annotation with __init__

When __init__ is defined, member values are validated by synthesizing a call to __init__. The _value_ annotation still constrains assignments to self._value_ inside __init__:

from enum import Enum

class Planet(Enum):
    _value_: int

    def __init__(self, value: int, mass: float, radius: float):
        self._value_ = value

    MERCURY = (1, 3.303e23, 2.4397e6)
    SATURN = "saturn"  # error: [invalid-assignment]

reveal_type(Planet.MERCURY.value)  # revealed: int
reveal_type(Planet.MERCURY._value_)  # revealed: int

Final-annotated members are also validated against __init__:

from enum import Enum
from typing import Final

class Planet(Enum):
    def __init__(self, mass: float, radius: float):
        self.mass = mass
        self.radius = radius

    MERCURY: Final = (3.303e23, 2.4397e6)
    BAD: Final = "not a planet"  # error: [invalid-assignment]

_value_ annotation incompatible with __init__

When _value_ and __init__ disagree, the assignment inside __init__ is flagged:

from enum import Enum

class Planet(Enum):
    _value_: str

    def __init__(self, value: int, mass: float, radius: float):
        self._value_ = value  # error: [invalid-assignment]

    MERCURY = (1, 3.303e23, 2.4397e6)
    SATURN = "saturn"  # error: [invalid-assignment]

reveal_type(Planet.MERCURY.value)  # revealed: str
reveal_type(Planet.MERCURY._value_)  # revealed: str

__init__ without _value_ annotation

When __init__ is defined but no explicit _value_ annotation exists, member values are validated against the __init__ signature. Values that are incompatible with __init__ are flagged:

from enum import Enum

class Planet2(Enum):
    def __init__(self, mass: float, radius: float):
        self.mass = mass
        self.radius = radius

    MERCURY = (3.303e23, 2.4397e6)
    VENUS = (4.869e24, 6.0518e6)
    INVALID = "not a planet"  # error: [invalid-assignment]

reveal_type(Planet2.MERCURY.value)  # revealed: Any
reveal_type(Planet2.MERCURY._value_)  # revealed: Any

Inherited _value_ annotation

A _value_ annotation on a parent enum is inherited by subclasses. Member values are validated against the inherited annotation, and .value uses the declared type:

from enum import Enum

class Base(Enum):
    _value_: int

class Child(Base):
    A = 1
    B = "not an int"  # error: [invalid-assignment]

reveal_type(Child.A.value)  # revealed: int

This also works through multiple levels of inheritance, where _value_ is declared on an intermediate class:

from enum import Enum

class Grandparent(Enum):
    pass

class Parent(Grandparent):
    _value_: int

class Child(Parent):
    A = 1
    B = "not an int"  # error: [invalid-assignment]

reveal_type(Child.A.value)  # revealed: int

Inherited __init__

A custom __init__ on a parent enum is inherited by subclasses. Member values are validated against the inherited __init__ signature:

from enum import Enum

class Base(Enum):
    def __init__(self, a: int, b: str):
        self._value_ = a

class Child(Base):
    A = (1, "foo")
    B = "should be checked against __init__"  # error: [invalid-assignment]

reveal_type(Child.A.value)  # revealed: Any

This also works through multiple levels of inheritance:

from enum import Enum

class Grandparent(Enum):
    def __init__(self, a: int, b: str):
        self._value_ = a

class Parent(Grandparent):
    pass

class Child(Parent):
    A = (1, "foo")
    B = "bad"  # error: [invalid-assignment]

reveal_type(Child.A.value)  # revealed: Any

Non-member attributes with disallowed type

Methods, callables, descriptors (including properties), and nested classes that are defined in the class are not treated as enum members:

from enum import Enum
from ty_extensions import enum_members
from typing import Callable, Literal

def identity(x) -> int:
    return x

class Descriptor:
    def __get__(self, instance, owner):
        return 0

class Answer(Enum):
    YES = 1
    NO = 2

    def some_method(self) -> None: ...
    @staticmethod
    def some_static_method() -> None: ...
    @classmethod
    def some_class_method(cls) -> None: ...

    some_callable = lambda x: 0
    declared_callable: Callable[[int], int] = identity
    function_reference = identity

    some_descriptor = Descriptor()

    @property
    def some_property(self) -> str:
        return ""

    class NestedClass: ...

# revealed: tuple[Literal["YES"], Literal["NO"]]
reveal_type(enum_members(Answer))

enum.property

Enum attributes that are defined using enum.property are not considered members:

[environment]
python-version = "3.11"
from enum import Enum, property as enum_property
from typing import Any
from ty_extensions import enum_members

class Answer(Enum):
    YES = 1
    NO = 2

    @enum_property
    def some_property(self) -> str:
        return "property value"

# revealed: tuple[Literal["YES"], Literal["NO"]]
reveal_type(enum_members(Answer))

Enum attributes defined using enum.property take precedence over generated attributes.

from enum import Enum, property as enum_property

class Choices(Enum):
    A = 1
    B = 2

    @enum_property
    def value(self) -> Any: ...

# TODO: This should be `Any` - overridden by `@enum_property`
reveal_type(Choices.A.value)  # revealed: Literal[1]

types.DynamicClassAttribute

Attributes defined using types.DynamicClassAttribute are not considered members:

from enum import Enum
from ty_extensions import enum_members
from types import DynamicClassAttribute

class Answer(Enum):
    YES = 1
    NO = 2

    @DynamicClassAttribute
    def dynamic_property(self) -> str:
        return "dynamic value"

# revealed: tuple[Literal["YES"], Literal["NO"]]
reveal_type(enum_members(Answer))

In stubs

Stubs can optionally use ... for the actual value:

from enum import Enum
from ty_extensions import enum_members
from typing import cast

class Color(Enum):
    RED = ...
    GREEN = cast(int, ...)
    BLUE = 3

# revealed: tuple[Literal["RED"], Literal["GREEN"], Literal["BLUE"]]
reveal_type(enum_members(Color))

Aliases

Enum members can have aliases, which are not considered separate members:

from enum import Enum
from ty_extensions import enum_members

class Answer(Enum):
    YES = 1
    NO = 2

    DEFINITELY = YES

# revealed: tuple[Literal["YES"], Literal["NO"]]
reveal_type(enum_members(Answer))

reveal_type(Answer.DEFINITELY)  # revealed: Literal[Answer.YES]

If a value is duplicated, we also treat that as an alias:

from enum import Enum

class Color(Enum):
    RED = 1
    GREEN = 2

    red = 1
    green = 2

# revealed: tuple[Literal["RED"], Literal["GREEN"]]
reveal_type(enum_members(Color))

# revealed: Literal[Color.RED]
reveal_type(Color.red)

Multiple aliases to the same member are also supported. This is a regression test for astral-sh/ty#1293:

from ty_extensions import enum_members

class ManyAliases(Enum):
    real_member = "real_member"
    alias1 = "real_member"
    alias2 = "real_member"
    alias3 = "real_member"

    other_member = "other_real_member"

# revealed: tuple[Literal["real_member"], Literal["other_member"]]
reveal_type(enum_members(ManyAliases))

reveal_type(ManyAliases.real_member)  # revealed: Literal[ManyAliases.real_member]
reveal_type(ManyAliases.alias1)  # revealed: Literal[ManyAliases.real_member]
reveal_type(ManyAliases.alias2)  # revealed: Literal[ManyAliases.real_member]
reveal_type(ManyAliases.alias3)  # revealed: Literal[ManyAliases.real_member]

reveal_type(ManyAliases.real_member.value)  # revealed: Literal["real_member"]
reveal_type(ManyAliases.real_member.name)  # revealed: Literal["real_member"]

reveal_type(ManyAliases.alias1.value)  # revealed: Literal["real_member"]
reveal_type(ManyAliases.alias1.name)  # revealed: Literal["real_member"]

reveal_type(ManyAliases.alias2.value)  # revealed: Literal["real_member"]
reveal_type(ManyAliases.alias2.name)  # revealed: Literal["real_member"]

reveal_type(ManyAliases.alias3.value)  # revealed: Literal["real_member"]
reveal_type(ManyAliases.alias3.name)  # revealed: Literal["real_member"]

Using auto()

[environment]
python-version = "3.11"
from enum import Enum, auto
from ty_extensions import enum_members

class Answer(Enum):
    YES = auto()
    NO = auto()

# revealed: tuple[Literal["YES"], Literal["NO"]]
reveal_type(enum_members(Answer))

reveal_type(Answer.YES.value)  # revealed: Literal[1]
reveal_type(Answer.NO.value)  # revealed: Literal[2]

class SingleMember(Enum):
    SINGLE = auto()

reveal_type(SingleMember.SINGLE.value)  # revealed: Literal[1]

Usages of auto() can be combined with manual value assignments:

class Mixed(Enum):
    MANUAL_1 = -1
    AUTO_1 = auto()
    MANUAL_2 = -2
    AUTO_2 = auto()

reveal_type(Mixed.MANUAL_1.value)  # revealed: Literal[-1]
reveal_type(Mixed.AUTO_1.value)  # revealed: Literal[1]
reveal_type(Mixed.MANUAL_2.value)  # revealed: Literal[-2]
reveal_type(Mixed.AUTO_2.value)  # revealed: Literal[2]

When using auto() with StrEnum, the value is the lowercase name of the member:

from enum import StrEnum, auto

class Answer(StrEnum):
    YES = auto()
    NO = auto()

reveal_type(Answer.YES.value)  # revealed: Literal["yes"]
reveal_type(Answer.NO.value)  # revealed: Literal["no"]

class SingleMember(StrEnum):
    SINGLE = auto()

reveal_type(SingleMember.SINGLE.value)  # revealed: Literal["single"]

Using auto() with IntEnum also works as expected. IntEnum declares _value_: int in typeshed, so .value is typed as int rather than a precise literal:

from enum import IntEnum, auto

class Answer(IntEnum):
    YES = auto()
    NO = auto()

reveal_type(Answer.YES.value)  # revealed: int
reveal_type(Answer.NO.value)  # revealed: int

As does using auto() for other enums that use int as a mixin:

from enum import Enum, auto

class Answer(int, Enum):
    YES = auto()
    NO = auto()

reveal_type(Answer.YES.value)  # revealed: Literal[1]
reveal_type(Answer.NO.value)  # revealed: Literal[2]

It's hard to predict what the effect of using auto() will be for an arbitrary non-integer mixin, so for anything that isn't a StrEnum and has a non-int mixin, we simply fallback to typeshed's annotation of Any for the value property:

from enum import Enum, auto

class A(str, Enum):
    X = auto()
    Y = auto()

reveal_type(A.X.value)  # revealed: Any

class B(bytes, Enum):
    X = auto()
    Y = auto()

reveal_type(B.X.value)  # revealed: Any

class C(tuple, Enum):
    X = auto()
    Y = auto()

reveal_type(C.X.value)  # revealed: Any

class D(float, Enum):
    X = auto()
    Y = auto()

reveal_type(D.X.value)  # revealed: Any

Combining aliases with auto():

from enum import Enum, auto

class Answer(Enum):
    YES = auto()
    NO = auto()

    DEFINITELY = YES

# TODO: This should ideally be `tuple[Literal["YES"], Literal["NO"]]`
# revealed: tuple[Literal["YES"], Literal["NO"], Literal["DEFINITELY"]]
reveal_type(enum_members(Answer))

auto() values are computed at runtime by the enum metaclass, so we skip validation against both _value_ annotations and custom __init__ signatures:

from enum import Enum, auto

class WithValue(Enum):
    _value_: int
    A = auto()
    B = auto()

reveal_type(WithValue.A.value)  # revealed: int

class WithInit(Enum):
    def __init__(self, mass: float, radius: float):
        self.mass = mass
        self.radius = radius

    MERCURY = (3.303e23, 2.4397e6)
    AUTO = auto()

reveal_type(WithInit.MERCURY.value)  # revealed: Any

member and nonmember

[environment]
python-version = "3.11"
from enum import Enum, auto, member, nonmember
from ty_extensions import enum_members

class Answer(Enum):
    YES = member(1)
    NO = member(2)
    OTHER = nonmember(17)

# revealed: tuple[Literal["YES"], Literal["NO"]]
reveal_type(enum_members(Answer))

# `nonmember` attributes are unwrapped to the inner value type when accessed.
# revealed: int
reveal_type(Answer.OTHER)

member can also be used as a decorator:

from enum import Enum, member
from ty_extensions import enum_members

class Answer(Enum):
    yes = member(1)
    no = member(2)

    @member
    def maybe(self) -> None:
        return

# revealed: tuple[Literal["yes"], Literal["no"], Literal["maybe"]]
reveal_type(enum_members(Answer))

Dunder and class-private names

An attribute with a name beginning with a double underscore is treated as a non-member. This includes both class-private names (not ending in __) and dunder names (ending in __). CPython's enum metaclass excludes all such names from membership:

from enum import Enum, IntEnum
from ty_extensions import enum_members

class Answer(Enum):
    YES = 1
    NO = 2

    __private_member = 3
    __maybe__ = 4

# revealed: tuple[Literal["YES"], Literal["NO"]]
reveal_type(enum_members(Answer))

Setting __module__ (a common pattern to control repr() and pickle behavior) does not make it an enum member, even when the value type differs from the enum's value type:

class ExitCode(IntEnum):
    OK = 0
    ERROR = 1

    __module__ = "my_package"  # no error, not a member

# revealed: tuple[Literal["OK"], Literal["ERROR"]]
reveal_type(enum_members(ExitCode))

Ignored names

An enum class can define a class symbol named _ignore_. This can be a string containing a whitespace-delimited list of names:

from enum import Enum
from ty_extensions import enum_members

class Answer(Enum):
    _ignore_ = "IGNORED _other_ignored       also_ignored"

    YES = 1
    NO = 2

    IGNORED = 3
    _other_ignored = "test"
    also_ignored = "test2"

# revealed: tuple[Literal["YES"], Literal["NO"]]
reveal_type(enum_members(Answer))

_ignore_ can also be a list of names:

class Answer2(Enum):
    _ignore_ = ["MAYBE", "_other"]

    YES = 1
    NO = 2

    MAYBE = 3
    _other = "test"

# TODO: This should be `tuple[Literal["YES"], Literal["NO"]]`
# revealed: tuple[Literal["YES"], Literal["NO"], Literal["MAYBE"], Literal["_other"]]
reveal_type(enum_members(Answer2))

Special names

Make sure that special names like name and value can be used for enum members (without conflicting with Enum.name and Enum.value):

from enum import Enum
from ty_extensions import enum_members

class Answer(Enum):
    name = 1
    value = 2

# revealed: tuple[Literal["name"], Literal["value"]]
reveal_type(enum_members(Answer))

reveal_type(Answer.name)  # revealed: Literal[Answer.name]
reveal_type(Answer.value)  # revealed: Literal[Answer.value]

Iterating over enum members

from enum import Enum

class Color(Enum):
    RED = 1
    GREEN = 2
    BLUE = 3

for color in Color:
    reveal_type(color)  # revealed: Color

# TODO: Should be `list[Color]`
reveal_type(list(Color))  # revealed: list[Unknown]

Methods / non-member attributes

Methods and non-member attributes defined in the enum class can be accessed on enum members:

from enum import Enum

class Answer(Enum):
    YES = 1
    NO = 2

    def is_yes(self) -> bool:
        return self == Answer.YES

    constant: int

reveal_type(Answer.YES.is_yes())  # revealed: bool
reveal_type(Answer.YES.constant)  # revealed: int

class MyEnum(Enum):
    def some_method(self) -> None:
        pass

class MyAnswer(MyEnum):
    YES = 1
    NO = 2

reveal_type(MyAnswer.YES.some_method())  # revealed: None

Accessing enum members from enum members / instances

from enum import Enum

class Answer(Enum):
    YES = 1
    NO = 2

reveal_type(Answer.YES.NO)  # revealed: Literal[Answer.NO]

def _(answer: Answer) -> None:
    reveal_type(answer.YES)  # revealed: Literal[Answer.YES]
    reveal_type(answer.NO)  # revealed: Literal[Answer.NO]

Accessing enum members from type[…]

from enum import Enum

class Answer(Enum):
    YES = 1
    NO = 2

def _(answer: type[Answer]) -> None:
    reveal_type(answer.YES)  # revealed: Literal[Answer.YES]
    reveal_type(answer.NO)  # revealed: Literal[Answer.NO]

Calling enum variants

from enum import Enum
from typing import Callable
import sys

class Printer(Enum):
    STDOUT = 1
    STDERR = 2

    def __call__(self, msg: str) -> None:
        if self == Printer.STDOUT:
            print(msg)
        elif self == Printer.STDERR:
            print(msg, file=sys.stderr)

Printer.STDOUT("Hello, world!")
Printer.STDERR("An error occurred!")

callable: Callable[[str], None] = Printer.STDOUT
callable("Hello again!")
callable = Printer.STDERR
callable("Another error!")

Special attributes on enum members

name and _name_

from enum import Enum
from typing import Literal

class Color(Enum):
    RED = 1
    GREEN = 2
    BLUE = 3

reveal_type(Color.RED._name_)  # revealed: Literal["RED"]

def _(red_or_blue: Literal[Color.RED, Color.BLUE]):
    reveal_type(red_or_blue.name)  # revealed: Literal["RED", "BLUE"]

def _(color: Color):
    reveal_type(color.name)  # revealed: Literal["RED", "GREEN", "BLUE"]

value and _value_

[environment]
python-version = "3.11"
from enum import Enum, StrEnum
from typing import Literal

class Color(Enum):
    RED = 1
    GREEN = 2
    BLUE = 3

reveal_type(Color.RED.value)  # revealed: Literal[1]
reveal_type(Color.RED._value_)  # revealed: Literal[1]

reveal_type(Color.GREEN.value)  # revealed: Literal[2]
reveal_type(Color.GREEN._value_)  # revealed: Literal[2]

def _(color: Color):
    reveal_type(color.value)  # revealed: Literal[1, 2, 3]

class Answer(StrEnum):
    YES = "yes"
    NO = "no"

reveal_type(Answer.YES.value)  # revealed: Literal["yes"]
reveal_type(Answer.YES._value_)  # revealed: Literal["yes"]

reveal_type(Answer.NO.value)  # revealed: Literal["no"]
reveal_type(Answer.NO._value_)  # revealed: Literal["no"]

def _(answer: Answer):
    reveal_type(answer.value)  # revealed: Literal["yes", "no"]

Properties of enum types

Implicitly final

An enum with one or more defined members cannot be subclassed. They are implicitly "final".

from enum import Enum

class Color(Enum):
    RED = 1
    GREEN = 2
    BLUE = 3

# error: [subclass-of-final-class] "Class `ExtendedColor` cannot inherit from final class `Color`"
class ExtendedColor(Color):
    YELLOW = 4

def f(color: Color):
    if isinstance(color, int):
        reveal_type(color)  # revealed: Never

An Enum subclass without any defined members can be subclassed:

from enum import Enum
from ty_extensions import enum_members

class MyEnum(Enum):
    def some_method(self) -> None:
        pass

class Answer(MyEnum):
    YES = 1
    NO = 2

# revealed: tuple[Literal["YES"], Literal["NO"]]
reveal_type(enum_members(Answer))

Meta-type

from enum import Enum

class Answer(Enum):
    YES = 1
    NO = 2

reveal_type(type(Answer.YES))  # revealed: <class 'Answer'>

class NoMembers(Enum): ...

def _(answer: Answer, no_members: NoMembers):
    reveal_type(type(answer))  # revealed: <class 'Answer'>
    reveal_type(type(no_members))  # revealed: type[NoMembers]

Cyclic references

from enum import Enum
from typing import Literal
from ty_extensions import enum_members

class Answer(Enum):
    YES = 1
    NO = 2

    @classmethod
    def yes(cls) -> "Literal[Answer.YES]":
        return Answer.YES

# revealed: tuple[Literal["YES"], Literal["NO"]]
reveal_type(enum_members(Answer))

Custom enum types

Enum classes can also be defined using a subclass of enum.Enum or any class that uses enum.EnumType (or a subclass thereof) as a metaclass. enum.EnumType was called enum.EnumMeta prior to Python 3.11.

Subclasses of Enum

from enum import Enum, EnumMeta

class CustomEnumSubclass(Enum):
    def custom_method(self) -> int:
        return 0

class EnumWithCustomEnumSubclass(CustomEnumSubclass):
    NO = 0
    YES = 1

reveal_type(EnumWithCustomEnumSubclass.NO)  # revealed: Literal[EnumWithCustomEnumSubclass.NO]
reveal_type(EnumWithCustomEnumSubclass.NO.custom_method())  # revealed: int

Enums with (subclasses of) EnumMeta as metaclass

[environment]
python-version = "3.9"
from enum import Enum, EnumMeta

class EnumWithEnumMetaMetaclass(metaclass=EnumMeta):
    # Using `EnumMeta` as a metaclass without inheriting `Enum` requires an `__init__`
    # method that will accept member values (TODO we could catch the lack of this):
    def __init__(self, val): ...
    NO = 0
    YES = 1

reveal_type(EnumWithEnumMetaMetaclass.NO)  # revealed: Literal[EnumWithEnumMetaMetaclass.NO]

class SubclassOfEnumMeta(EnumMeta): ...

class EnumWithSubclassOfEnumMetaMetaclass(metaclass=SubclassOfEnumMeta):
    def __init__(self, val): ...
    NO = 0
    YES = 1

reveal_type(EnumWithSubclassOfEnumMetaMetaclass.NO)  # revealed: Literal[EnumWithSubclassOfEnumMetaMetaclass.NO]

# Attributes `.value` and `.name` can *not* be accessed on members of these enums:

# error: [unresolved-attribute]
EnumWithSubclassOfEnumMetaMetaclass.NO.value
# error: [unresolved-attribute]
EnumWithSubclassOfEnumMetaMetaclass.NO.name

# But the internal underscore attributes are available:

reveal_type(EnumWithSubclassOfEnumMetaMetaclass.NO._value_)  # revealed: Any
reveal_type(EnumWithSubclassOfEnumMetaMetaclass.NO._name_)  # revealed: Literal["NO"]

def _(x: EnumWithSubclassOfEnumMetaMetaclass):
    # error: [unresolved-attribute]
    x.value
    # error: [unresolved-attribute]
    x.name
    reveal_type(x._value_)  # revealed: Any
    reveal_type(x._name_)  # revealed: Literal["NO", "YES"]

Enums with (subclasses of) EnumType as metaclass

In Python 3.11, the meta-type was renamed to EnumType.

[environment]
python-version = "3.11"
from enum import Enum, EnumType

class EnumWithEnumMetaMetaclass(metaclass=EnumType):
    def __init__(self, val): ...
    NO = 0
    YES = 1

reveal_type(EnumWithEnumMetaMetaclass.NO)  # revealed: Literal[EnumWithEnumMetaMetaclass.NO]

class SubclassOfEnumMeta(EnumType): ...

class EnumWithSubclassOfEnumMetaMetaclass(metaclass=SubclassOfEnumMeta):
    def __init__(self, val): ...
    NO = 0
    YES = 1

reveal_type(EnumWithSubclassOfEnumMetaMetaclass.NO)  # revealed: Literal[EnumWithSubclassOfEnumMetaMetaclass.NO]

# Attributes `.value` and `.name` can *not* be accessed on members of these enums:

# error: [unresolved-attribute]
EnumWithSubclassOfEnumMetaMetaclass.NO.value
# error: [unresolved-attribute]
EnumWithSubclassOfEnumMetaMetaclass.NO.name

# But the internal underscore attributes are available:

reveal_type(EnumWithSubclassOfEnumMetaMetaclass.NO._value_)  # revealed: Any
reveal_type(EnumWithSubclassOfEnumMetaMetaclass.NO._name_)  # revealed: Literal["NO"]

def _(x: EnumWithSubclassOfEnumMetaMetaclass):
    # error: [unresolved-attribute]
    x.value
    # error: [unresolved-attribute]
    x.name
    reveal_type(x._value_)  # revealed: Any
    reveal_type(x._name_)  # revealed: Literal["NO", "YES"]

Function syntax

String names (positional)

from enum import Enum
from ty_extensions import enum_members

Color = Enum("Color", "RED GREEN BLUE")

# revealed: tuple[Literal["RED"], Literal["GREEN"], Literal["BLUE"]]
reveal_type(enum_members(Color))

Color = Enum("Color", "RED, GREEN, BLUE")

# revealed: tuple[Literal["RED"], Literal["GREEN"], Literal["BLUE"]]
reveal_type(enum_members(Color))

String names (keyword)

from enum import Enum
from ty_extensions import enum_members

Color = Enum("Color", names="RED GREEN BLUE")

# revealed: tuple[Literal["RED"], Literal["GREEN"], Literal["BLUE"]]
reveal_type(enum_members(Color))

List/tuple of tuples

from enum import Enum
from ty_extensions import enum_members

Color = Enum("Color", [("RED", 1), ("GREEN", 2), ("BLUE", 3)])

# revealed: tuple[Literal["RED"], Literal["GREEN"], Literal["BLUE"]]
reveal_type(enum_members(Color))

Color = Enum("Color", (("RED", 1), ("GREEN", 2), ("BLUE", 3)))

# revealed: tuple[Literal["RED"], Literal["GREEN"], Literal["BLUE"]]
reveal_type(enum_members(Color))

List of strings

from enum import Enum
from ty_extensions import enum_members

Color = Enum("Color", ["RED", "GREEN", "BLUE"])

# revealed: tuple[Literal["RED"], Literal["GREEN"], Literal["BLUE"]]
reveal_type(enum_members(Color))

Dict mapping

from enum import Enum
from ty_extensions import enum_members

Color = Enum("Color", {"RED": 1, "GREEN": 2, "BLUE": 3})

# revealed: tuple[Literal["RED"], Literal["GREEN"], Literal["BLUE"]]
reveal_type(enum_members(Color))

reveal_type(Color.RED.value)  # revealed: Literal[1]
reveal_type(Color.GREEN.value)  # revealed: Literal[2]
reveal_type(Color.BLUE.value)  # revealed: Literal[3]

Dict mapping with auto()

from enum import Enum, auto
from ty_extensions import enum_members

Color = Enum("Color", {"RED": auto(), "GREEN": auto(), "BLUE": auto()})

# revealed: tuple[Literal["RED"], Literal["GREEN"], Literal["BLUE"]]
reveal_type(enum_members(Color))

reveal_type(Color.RED.value)  # revealed: Literal[1]
reveal_type(Color.GREEN.value)  # revealed: Literal[2]
reveal_type(Color.BLUE.value)  # revealed: Literal[3]

When mixing explicit values with auto() in a dict, the auto value is derived from the previous member's value, not from start + index:

from enum import Enum, auto
from ty_extensions import enum_members

Mixed = Enum("Mixed", {"A": 10, "B": auto(), "C": auto()})

# revealed: tuple[Literal["A"], Literal["B"], Literal["C"]]
reveal_type(enum_members(Mixed))

reveal_type(Mixed.A.value)  # revealed: Literal[10]
reveal_type(Mixed.B.value)  # revealed: Literal[11]
reveal_type(Mixed.C.value)  # revealed: Literal[12]

Duplicate member names

Duplicate member names raise TypeError at runtime. We degrade to unknown members rather than synthesizing a broken enum.

from enum import Enum
from ty_extensions import enum_members

E1 = Enum("E1", "A A")
reveal_type(enum_members(E1))  # revealed: Unknown

E2 = Enum("E2", ["A", "A"])
reveal_type(enum_members(E2))  # revealed: Unknown

E3 = Enum("E3", [("A", 1), ("A", 2)])
reveal_type(enum_members(E3))  # revealed: Unknown

Unknown members: inherited attribute access

When members are unknown, own member access returns Unknown, but inherited attributes from the enum base class should still resolve through the MRO.

from enum import Enum

names: list[str] = ["A", "B"]
E = Enum("E", names)

# inherited class attributes resolve from Enum base
reveal_type(E.__members__)  # revealed: MappingProxyType[str, E]

# own member access is unknown (can't tell if it exists)
reveal_type(E.FOO)  # revealed: Unknown

Too many positional args

Enum(value, names, *, ...) only accepts two positional args at runtime.

from enum import Enum
from ty_extensions import enum_members

# error: [too-many-positional-arguments]
Color = Enum("Color", "RED", "GREEN", "BLUE")

reveal_type(enum_members(Color))  # revealed: Unknown

No positional args

from enum import Enum

# this is invalid at runtime but should not panic
Color = Enum()

reveal_type(Color)  # revealed: @Todo(functional `Enum` syntax)

Non-literal name

Non-literal names should still be recognized as creating an enum class.

from enum import Enum

def make_enum(name: str, labels: tuple[str, ...]) -> type[Enum]:
    result = Enum(name.title(), labels, module=__name__)
    reveal_type(result)  # revealed: type[Enum]
    return result

Non-string name

from enum import Enum

# error: [invalid-argument-type]
Color = Enum(123, "RED GREEN BLUE")

Unknown keyword arguments

from enum import Enum

# error: [unknown-argument]
Color = Enum("Color", "RED GREEN BLUE", bad_kwarg=True)

boundary keyword (Python 3.11+)

Available on 3.11+

[environment]
python-version = "3.11"
from enum import Flag

Perm = Flag("Perm", "READ WRITE EXECUTE", boundary=None)

Rejected before 3.11

[environment]
python-version = "3.10"
from enum import Flag

# error: [unknown-argument]
Perm = Flag("Perm", "READ WRITE EXECUTE", boundary=None)

StrEnum function syntax

[environment]
python-version = "3.11"
from enum import StrEnum
from ty_extensions import enum_members

Color = StrEnum("Color", "RED GREEN BLUE")

# revealed: tuple[Literal["RED"], Literal["GREEN"], Literal["BLUE"]]
reveal_type(enum_members(Color))

reveal_type(Color.RED.value)  # revealed: Literal["red"]
reveal_type(Color.GREEN.value)  # revealed: Literal["green"]
reveal_type(Color.BLUE.value)  # revealed: Literal["blue"]

Custom start value

from enum import Enum

Color = Enum("Color", "RED GREEN BLUE", start=0)

reveal_type(Color.RED.value)  # revealed: Literal[0]
reveal_type(Color.GREEN.value)  # revealed: Literal[1]
reveal_type(Color.BLUE.value)  # revealed: Literal[2]

Type mixin

from enum import Enum

Http = Enum("Http", "OK NOT_FOUND", type=int)

reveal_type(Http.OK.value)  # revealed: Literal[1]
reveal_type(Http.NOT_FOUND.value)  # revealed: Literal[2]

IntEnum function syntax

from enum import IntEnum
from ty_extensions import enum_members

Color = IntEnum("Color", "RED GREEN BLUE")

# revealed: tuple[Literal["RED"], Literal["GREEN"], Literal["BLUE"]]
reveal_type(enum_members(Color))

Flag function syntax

from enum import Flag
from ty_extensions import enum_members

Perm = Flag("Perm", "READ WRITE EXECUTE")

# revealed: tuple[Literal["READ"], Literal["WRITE"], Literal["EXECUTE"]]
reveal_type(enum_members(Perm))

reveal_type(Perm.READ.value)  # revealed: Literal[1]
reveal_type(Perm.WRITE.value)  # revealed: Literal[2]
reveal_type(Perm.EXECUTE.value)  # revealed: Literal[4]

IntFlag function syntax

from enum import IntFlag
from ty_extensions import enum_members

Perm = IntFlag("Perm", "READ WRITE EXECUTE")

# revealed: tuple[Literal["READ"], Literal["WRITE"], Literal["EXECUTE"]]
reveal_type(enum_members(Perm))

reveal_type(Perm.READ.value)  # revealed: Literal[1]
reveal_type(Perm.WRITE.value)  # revealed: Literal[2]
reveal_type(Perm.EXECUTE.value)  # revealed: Literal[4]

Large start value (overflow guard)

Values that would overflow i64 should gracefully widen to int.

from enum import Enum, Flag

Big = Enum("Big", "A B", start=9223372036854775807)

reveal_type(Big.A.value)  # revealed: Literal[9223372036854775807]
reveal_type(Big.B.value)  # revealed: int

BigFlag = Flag("BigFlag", "X Y", start=4611686018427387904)

reveal_type(BigFlag.X.value)  # revealed: Literal[4611686018427387904]
reveal_type(BigFlag.Y.value)  # revealed: int

Exhaustiveness checking

if statements

from enum import Enum
from typing_extensions import assert_never

class Color(Enum):
    RED = 1
    GREEN = 2
    BLUE = 3

def color_name(color: Color) -> str:
    if color is Color.RED:
        return "Red"
    elif color is Color.GREEN:
        return "Green"
    elif color is Color.BLUE:
        return "Blue"
    else:
        assert_never(color)

# No `invalid-return-type` error here because the implicit `else` branch is detected as unreachable:
def color_name_without_assertion(color: Color) -> str:
    if color is Color.RED:
        return "Red"
    elif color is Color.GREEN:
        return "Green"
    elif color is Color.BLUE:
        return "Blue"

def color_name_misses_one_variant(color: Color) -> str:
    if color is Color.RED:
        return "Red"
    elif color is Color.GREEN:
        return "Green"
    else:
        assert_never(color)  # error: [type-assertion-failure] "Type `Literal[Color.BLUE]` is not equivalent to `Never`"

class Singleton(Enum):
    VALUE = 1

def singleton_check(value: Singleton) -> str:
    if value is Singleton.VALUE:
        return "Singleton value"
    else:
        assert_never(value)

match statements

[environment]
python-version = "3.10"
from enum import Enum
from typing_extensions import assert_never

class Color(Enum):
    RED = 1
    GREEN = 2
    BLUE = 3

def color_name(color: Color) -> str:
    match color:
        case Color.RED:
            return "Red"
        case Color.GREEN:
            return "Green"
        case Color.BLUE:
            return "Blue"
        case _:
            assert_never(color)

def color_name_without_assertion(color: Color) -> str:
    match color:
        case Color.RED:
            return "Red"
        case Color.GREEN:
            return "Green"
        case Color.BLUE:
            return "Blue"

def color_name_misses_one_variant(color: Color) -> str:
    match color:
        case Color.RED:
            return "Red"
        case Color.GREEN:
            return "Green"
        case _:
            assert_never(color)  # error: [type-assertion-failure] "Type `Literal[Color.BLUE]` is not equivalent to `Never`"

class Singleton(Enum):
    VALUE = 1

def singleton_check(value: Singleton) -> str:
    match value:
        case Singleton.VALUE:
            return "Singleton value"
        case _:
            assert_never(value)

__eq__ and __ne__

No __eq__ or __ne__ overrides

from enum import Enum

class Color(Enum):
    RED = 1
    GREEN = 2

reveal_type(Color.RED == Color.RED)  # revealed: Literal[True]
reveal_type(Color.RED != Color.RED)  # revealed: Literal[False]

Overridden __eq__

from enum import Enum

class Color(Enum):
    RED = 1
    GREEN = 2

    def __eq__(self, other: object) -> bool:
        return False

reveal_type(Color.RED == Color.RED)  # revealed: bool

Overridden __ne__

from enum import Enum

class Color(Enum):
    RED = 1
    GREEN = 2

    def __ne__(self, other: object) -> bool:
        return False

reveal_type(Color.RED != Color.RED)  # revealed: bool

Generic enums are invalid

Enum classes cannot be generic. Python does not support generic enums, and attempting to create one will result in a TypeError at runtime.

PEP 695 syntax

Using PEP 695 type parameters on an enum is invalid:

[environment]
python-version = "3.12"
from enum import Enum

# error: [invalid-generic-enum] "Enum class `E` cannot be generic"
class E[T](Enum):
    A = 1
    B = 2

Legacy Generic base class

Inheriting from both Enum and Generic[T] is also invalid:

from enum import Enum
from typing import Generic, TypeVar

T = TypeVar("T")

# error: [invalid-generic-enum] "Enum class `F` cannot be generic"
class F(Enum, Generic[T]):
    A = 1
    B = 2

Swapped order (Generic first)

The order of bases doesn't matter; it's still invalid:

from enum import Enum
from typing import Generic, TypeVar

T = TypeVar("T")

# error: [invalid-generic-enum] "Enum class `G` cannot be generic"
class G(Generic[T], Enum):
    A = 1
    B = 2

Enum subclasses

Subclasses of enum base classes also cannot be generic:

[environment]
python-version = "3.12"
from enum import Enum, IntEnum
from typing import Generic, TypeVar

T = TypeVar("T")

# error: [invalid-generic-enum] "Enum class `MyIntEnum` cannot be generic"
class MyIntEnum[T](IntEnum):
    A = 1

# error: [invalid-generic-enum] "Enum class `MyFlagEnum` cannot be generic"
class MyFlagEnum(IntEnum, Generic[T]):
    A = 1

Custom enum base class

Even with custom enum subclasses that don't have members, they cannot be made generic:

[environment]
python-version = "3.12"
from enum import Enum
from typing import Generic, TypeVar

T = TypeVar("T")

class MyEnumBase(Enum):
    def some_method(self) -> None: ...

# error: [invalid-generic-enum] "Enum class `MyEnum` cannot be generic"
class MyEnum[T](MyEnumBase):
    A = 1

References