# Enums ## Basic ```py from enum import Enum class Color(Enum): RED = 1 GREEN = 2 BLUE = 3 reveal_type(Color.RED) # revealed: @Todo(Attribute access on enum classes) reveal_type(Color.RED.value) # revealed: @Todo(Attribute access on enum classes) # TODO: Should be `Color` or `Literal[Color.RED]` reveal_type(Color["RED"]) # revealed: Unknown # TODO: Could be `Literal[Color.RED]` to be more precise reveal_type(Color(1)) # revealed: Color reveal_type(Color.RED in Color) # revealed: bool ``` ## Enum members ### Basic Simple enums with integer or string values: ```py 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` ```py 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)) ``` ### Declared non-member attributes Attributes on the enum class that are declared are not considered members of the enum: ```py from enum import Enum from ty_extensions import enum_members class Answer(Enum): YES = 1 NO = 2 non_member_1: int # TODO: this could be considered an error: non_member_1: str = "some value" # revealed: tuple[Literal["YES"], Literal["NO"]] reveal_type(enum_members(Answer)) ``` ### 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: ```py 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: ```toml [environment] python-version = "3.11" ``` ```py from enum import Enum, property as enum_property 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)) ``` ### `types.DynamicClassAttribute` Attributes defined using `types.DynamicClassAttribute` are not considered members: ```py 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: ```pyi 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: ```py 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)) ``` ### Using `auto()` ```py 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)) ``` Combining aliases with `auto()`: ```py 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)) ``` ### `member` and `nonmember` ```toml [environment] python-version = "3.11" ``` ```py 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)) ``` `member` can also be used as a decorator: ```py 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)) ``` ### Class-private names An attribute with a [class-private name] (beginning with, but not ending in, a double underscore) is treated as a non-member: ```py from enum import Enum from ty_extensions import enum_members class Answer(Enum): YES = 1 NO = 2 __private_member = 3 __maybe__ = 4 # revealed: tuple[Literal["YES"], Literal["NO"], Literal["__maybe__"]] reveal_type(enum_members(Answer)) ``` ### Ignored names An enum class can define a class symbol named `_ignore_`. This can be a string containing a whitespace-delimited list of names: ```py 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: ```py 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`): ```py 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)) # TODO: These should be `Answer` or `Literal[Answer.name]`/``Literal[Answer.value]` reveal_type(Answer.name) # revealed: @Todo(Attribute access on enum classes) reveal_type(Answer.value) # revealed: @Todo(Attribute access on enum classes) ``` ## Iterating over enum members ```py from enum import Enum class Color(Enum): RED = 1 GREEN = 2 BLUE = 3 for color in Color: # TODO: Should be `Color` reveal_type(color) # revealed: Unknown # TODO: Should be `list[Color]` reveal_type(list(Color)) # revealed: list[Unknown] ``` ## Properties of enum types ### Implicitly final An enum with one or more defined members cannot be subclassed. They are implicitly "final". ```py from enum import Enum class Color(Enum): RED = 1 GREEN = 2 BLUE = 3 # TODO: This should emit an error class ExtendedColor(Color): YELLOW = 4 def f(color: Color): if isinstance(color, int): # TODO: This should be `Never` reveal_type(color) # revealed: Color & int ``` An `Enum` subclass without any defined members can be subclassed: ```py 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)) ``` ## Custom enum types To do: ## Function syntax To do: ## Exhaustiveness checking To do ## References - Typing spec: - Documentation: [class-private name]: https://docs.python.org/3/reference/lexical_analysis.html#reserved-classes-of-identifiers