crates/ty_python_semantic/resources/mdtest/enums.md
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
For standard-library enum classes, we preserve literal .value types when we can model how the data
type constructs each value. The inherited _value_ annotation remains the fallback when we cannot,
or when accessing .value on the enum class as a whole:
from enum import IntEnum, auto
from typing import Literal
class Integer(IntEnum):
ONE = 1
TRUE = True
TWO = 2
reveal_type(Integer.ONE.value) # revealed: Literal[1]
reveal_type(Integer.ONE._value_) # revealed: Literal[1]
reveal_type(Integer.TRUE.value) # revealed: Literal[1]
reveal_type(Integer.TRUE) # revealed: Literal[Integer.ONE]
def _(value: Integer):
reveal_type(value.value) # revealed: int
class ConvertedGenerated(IntEnum):
@staticmethod
def _generate_next_value_(name, start, count, last_values) -> Literal["1"]:
return "1"
ONE = auto()
reveal_type(ConvertedGenerated.ONE.value) # revealed: int
from enum import Enum, IntEnum
class Number(Enum):
ONE = 1
TWO = 2
reveal_type(Number(1)) # revealed: Number
reveal_type(Number(value=1)) # revealed: Number
class MixedInt(IntEnum):
ONE = 1
TWO = 2
reveal_type(MixedInt(1)) # revealed: MixedInt
class MixedStr(str, Enum):
RED = "red"
BLUE = "blue"
reveal_type(MixedStr("red")) # revealed: MixedStr
class Maybe(Enum):
NONE = None
SOME = "some"
reveal_type(Maybe(None)) # revealed: Maybe
class Planet(Enum):
_value_: int
def __init__(self, value: int, mass: float, radius: float):
self._value_ = value
MERCURY = (1, 3.303e23, 2.4397e6)
VENUS = (2, 4.869e24, 6.0518e6)
# TODO: `Planet(1)` raises `ValueError` at runtime. `EnumType.__call__` accepts positional
# arguments only, then forwards them to the enum's `__new__` / `__init__`, so multi-argument
# enum members still require the full positional member payload (for example `Planet(1, ...)`).
reveal_type(Planet(1)) # revealed: Planet
reveal_type(Planet(1, 3.303e23, 2.4397e6)) # revealed: Planet
class EmptyEnum(Enum): ...
# TODO: these raise `TypeError` at runtime, but we do not yet emit diagnostics for them.
reveal_type(EmptyEnum(foo=1)) # revealed: EmptyEnum
reveal_type(EmptyEnum(1, 2)) # revealed: EmptyEnum
Dynamic = Enum("Dynamic", {"RED": "red", "GREEN": "green"})
reveal_type(Dynamic("red")) # revealed: Dynamic
[environment]
python-version = "3.12"
from enum import Enum
class Triple(Enum):
XYZ = 1, 2, 3
OTHER = 4, 5, 6
reveal_type(Triple(1, 2, 3)) # revealed: Triple
Simple enums with integer or string values:
from enum import Enum
from ty_extensions._internal 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))
IntEnumfrom enum import IntEnum
from ty_extensions._internal 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))
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._internal 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._internal import enum_members
class Answer(Enum):
YES: Final = 1
NO: Final = 2
# revealed: tuple[Literal["YES"], Literal["NO"]]
reveal_type(enum_members(Answer))
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
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 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]
Statically unreachable declarations should be ignored when deciding whether a name is an enum member:
from enum import Enum
from ty_extensions._internal 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]
_value_ annotationIf 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_ annotationWhen __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
__init__An opaque __init__ shadows a function with the same name, so we can't validate members against it
(although a separate __new__ still validates the member arguments). Without an explicit _value_
annotation, .value becomes Any:
from enum import Enum
from typing import Any, cast
def external_init(self: Any, value: object) -> None: ...
class ReassignedInit(Enum):
_value_: int
def __init__(self, value: int) -> None: ...
__init__ = cast(Any, external_init)
A = "accepted by the assigned hook"
reveal_type(ReassignedInit.A.value) # revealed: int
class AssignedInitWithFunctionNew(Enum):
def __new__(cls, value: int): ...
__init__ = external_init
A = "not an int" # error: [invalid-assignment]
class FunctionInitBase(Enum):
def __init__(self, value: int) -> None: ...
class AssignedInitMiddle(FunctionInitBase):
__init__ = cast(Any, external_init)
class AssignedInitChild(AssignedInitMiddle):
A = "accepted by the assigned hook"
reveal_type(AssignedInitChild.A.value) # revealed: Any
def assigned_init_instance(value: AssignedInitChild) -> None:
reveal_type(value.value) # revealed: Any
__new__ without _value_ annotationWhen __new__ is defined but no explicit _value_ annotation exists, member RHS values are passed
to __new__, but the method can assign _value_ independently. In this case, .value falls back
to Any:
from enum import Enum
class Connector(Enum):
def __new__(cls, value: str, connector_id: int) -> "Connector":
obj = object.__new__(cls)
obj._value_ = value
obj.connector_id = connector_id
return obj
GITHUB = ("github", 1)
reveal_type(Connector.GITHUB.value) # revealed: Any
reveal_type(Connector.GITHUB._value_) # revealed: Any
An explicit _value_ annotation still takes precedence:
from enum import Enum
class AnnotatedConnector(Enum):
_value_: str
def __new__(cls, value: str, connector_id: int = 0) -> "AnnotatedConnector":
obj = object.__new__(cls)
obj._value_ = value
obj.connector_id = connector_id
return obj
GITHUB = "github"
reveal_type(AnnotatedConnector.GITHUB.value) # revealed: str
reveal_type(AnnotatedConnector.GITHUB._value_) # revealed: str
Even when a custom __new__ means that aliases cannot be determined, we assume that an enum
member's declaration name is canonical when inferring .name:
from enum import IntEnum
class CustomInteger(IntEnum):
def __new__(cls, value: int) -> "CustomInteger":
obj = int.__new__(cls, value)
obj._value_ = value
return obj
VALUE = 1
ALIAS = 1
reveal_type(CustomInteger.ALIAS.name) # revealed: Literal["ALIAS"]
_value_ annotationA _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
__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
__new__A custom __new__ on a parent enum is inherited by subclasses. Without an explicit _value_
annotation, subclass member values remain dynamic:
from enum import Enum
class Base(Enum):
def __new__(cls, value: str, connector_id: int) -> "Base":
obj = object.__new__(cls)
obj._value_ = value
obj.connector_id = connector_id
return obj
class Child(Base):
GITHUB = ("github", 1)
reveal_type(Child.GITHUB.value) # revealed: Any
reveal_type(Child.GITHUB._value_) # revealed: Any
An explicit _value_ annotation on the subclass still takes precedence:
from enum import Enum
class Base(Enum):
def __new__(cls, value: str, connector_id: int = 0) -> "Base":
obj = object.__new__(cls)
obj._value_ = value
obj.connector_id = connector_id
return obj
class Child(Base):
_value_: str
GITHUB = "github"
reveal_type(Child.GITHUB.value) # revealed: str
reveal_type(Child.GITHUB._value_) # revealed: str
Member values are still validated against the inherited __new__ signature, even when _value_ is
explicitly annotated:
from enum import Enum
class Base(Enum):
def __new__(cls, value: int, connector_id: int = 0) -> "Base":
obj = object.__new__(cls)
obj._value_ = value
obj.connector_id = connector_id
return obj
class Child(Base):
_value_: str
GITHUB = "github" # error: [invalid-assignment]
__new_member__An inherited __new_member__ takes precedence over the default enum constructor and can replace the
member value:
from enum import Enum
class Base(Enum):
def __new_member__(cls: type["Base"], value: int) -> "Base":
obj = object.__new__(cls)
obj._value_ = str(value)
return obj
class Child(Base):
VALUE = 1
reveal_type(Child.VALUE.value) # revealed: Any
EnumType saves an enum's user-defined __new__ as that class's __new_member__. The immediate
parent's __new__ therefore takes precedence over an explicit __new_member__ in a grandparent:
from enum import Enum
class Grandparent(Enum):
def __new_member__(cls: type["Grandparent"], value: str) -> "Grandparent":
obj = object.__new__(cls)
obj._value_ = value
return obj
class Parent(Grandparent):
def __new__(cls, value: int) -> "Parent":
obj = object.__new__(cls)
obj._value_ = value
return obj
class Child(Parent):
VALID = 1
INVALID = "not an int" # error: [invalid-assignment]
reveal_type(Child.VALID.value) # revealed: Any
__new__A user-defined __new__ on a data-type mixin constructs the scalar payload and can transform the
declared member value. Members are validated against its signature, and their .value types remain
dynamic when we cannot model the transformation:
from enum import Enum
from ty_extensions._internal import enum_members
class OffsetInt(int):
def __new__(cls, value: int) -> "OffsetInt":
return int.__new__(cls, value + 1)
class OffsetEnum(OffsetInt, Enum):
VALID = 1
INVALID = "not an int" # error: [invalid-assignment]
reveal_type(OffsetEnum.VALID.value) # revealed: Any
class WeirdInt(int):
def __new__(cls, value: int) -> "WeirdInt":
return int.__new__(cls, 100 if value is False else value)
class EmptyWeirdEnum(WeirdInt, Enum):
pass
class InheritedWeirdEnum(EmptyWeirdEnum):
FROM_BOOL = False
FROM_INT = 0
OTHER = 2
reveal_type(InheritedWeirdEnum.FROM_BOOL.value) # revealed: Any
reveal_type(InheritedWeirdEnum.FROM_INT) # revealed: Literal[InheritedWeirdEnum.FROM_INT]
reveal_type(enum_members(InheritedWeirdEnum)) # revealed: Unknown
An enum with an int or str data type stores the value produced by that type's constructor.
Aliases are determined from the constructed values rather than the original assignments:
from enum import Enum
from ty_extensions._internal import enum_members
from typing import Literal
class IntegerValues(int, Enum):
FALSE = False
ZERO = 0
reveal_type(IntegerValues.FALSE.value) # revealed: Literal[0]
# revealed: tuple[Literal["FALSE"]]
reveal_type(enum_members(IntegerValues))
class StringValues(str, Enum):
INTEGER = 1
STRING = "1"
BOOLEAN = False
BOOLEAN_STRING = "False"
reveal_type(StringValues.INTEGER.value) # revealed: Literal["1"]
reveal_type(StringValues.BOOLEAN.value) # revealed: Literal["False"]
# revealed: tuple[Literal["INTEGER"], Literal["BOOLEAN"]]
reveal_type(enum_members(StringValues))
def union_member_value(value: Literal[False, 2]):
class UnionValues(int, Enum):
MEMBER = value
reveal_type(UnionValues.MEMBER.value) # revealed: Literal[0, 2]
class IntegerBase(int, Enum):
pass
class InheritedValues(IntegerBase):
FALSE = False
ZERO = 0
reveal_type(InheritedValues.FALSE.value) # revealed: Literal[0]
# revealed: tuple[Literal["FALSE"]]
reveal_type(enum_members(InheritedValues))
Non-member declarations do not make alias detection inconclusive:
from enum import Enum
from ty_extensions._internal import enum_members
class ValuesWithHelper(int, Enum):
VALUE = 1
class Helper:
pass
ALIAS = 1
# revealed: tuple[Literal["VALUE"]]
reveal_type(enum_members(ValuesWithHelper))
When a built-in conversion cannot be modeled precisely, its aliases remain unknown:
from enum import Enum
from ty_extensions._internal import enum_members
class ParsedIntegerValues(int, Enum):
FIRST = "1"
SECOND = "1"
reveal_type(ParsedIntegerValues.FIRST is ParsedIntegerValues.SECOND) # revealed: bool
# TODO really should be `Literal["FIRST"]` since its a known alias
reveal_type(ParsedIntegerValues.SECOND.name) # revealed: Literal["SECOND"]
reveal_type(enum_members(ParsedIntegerValues)) # revealed: Unknown
Other built-in data types retain exact assigned values when no coercion is needed:
from enum import Enum
from ty_extensions._internal import enum_members
class ByteValues(bytes, Enum):
VALUE = b"value"
ALIAS = b"value"
reveal_type(ByteValues.VALUE.value) # revealed: Literal[b"value"]
# revealed: tuple[Literal["VALUE"]]
reveal_type(enum_members(ByteValues))
If the data type would coerce the assigned value, its value and aliases remain unknown:
from enum import Enum
from ty_extensions._internal import enum_members
class CoercingByteValues(bytes, Enum):
FROM_INT = 1
FROM_BYTES = b"\0"
reveal_type(CoercingByteValues.FROM_INT.value) # revealed: Any
reveal_type(CoercingByteValues.FROM_INT is CoercingByteValues.FROM_BYTES) # revealed: bool
reveal_type(enum_members(CoercingByteValues)) # revealed: Unknown
User-defined data types remain opaque even when they inherit from int or str without overriding
any methods. Their construction, attribute access, equality, and hashing can all differ from the
built-in type:
from enum import Enum
from ty_extensions._internal import enum_members
class CustomInt(int):
pass
class CustomValues(CustomInt, Enum):
FALSE = False
ZERO = 0
reveal_type(CustomValues.FALSE.value) # revealed: Any
reveal_type(CustomValues.FALSE is CustomValues.ZERO) # revealed: bool
reveal_type(CustomValues.ZERO.name) # revealed: Literal["ZERO"]
reveal_type(enum_members(CustomValues)) # revealed: Unknown
A user-defined behavior base can still affect member construction and attribute access, so it keeps the enum's values opaque even when a separate base selects a built-in data type:
class Behavior:
pass
class ValuesWithBehavior(Behavior, int, Enum):
FALSE = False
ZERO = 0
reveal_type(ValuesWithBehavior.FALSE.value) # revealed: Any
__new__Assigning to __new__ can prevent us from validating members against its signature or inferring
.value from the member right-hand side. Even if we can't analyze __new__, though, we still
respect an explicit _value_ annotation, and __init__ can still validate the member arguments:
from enum import Enum
from ty_extensions._internal import enum_members
from typing import Any, cast
def external_new(cls: type[Any], value: object) -> Any: ...
class ReassignedNew(Enum):
_value_: int
def __new__(cls, value: int): ...
__new__ = cast(Any, staticmethod(external_new))
A = "accepted by the assigned hook"
B = "accepted by the assigned hook"
reveal_type(ReassignedNew.A.value) # revealed: int
# revealed: tuple[Literal["A"], Literal["B"]]
reveal_type(enum_members(ReassignedNew))
class AssignedNewWithFunctionInit(Enum):
__new__ = staticmethod(external_new)
def __init__(self, value: int) -> None: ...
A = "not an int" # error: [invalid-assignment]
An assigned hook also shadows same-name function hooks from classes later in the MRO. We should not validate members against the shadowed method:
class FunctionNewBase(Enum):
def __new__(cls, value: int): ...
class AssignedNewMiddle(FunctionNewBase):
__new__ = staticmethod(external_new)
class AssignedNewChild(AssignedNewMiddle):
A = "accepted by the assigned hook"
reveal_type(AssignedNewChild.A.value) # revealed: Any
A custom EnumMeta metaclass can rewrite member values before the stdlib enum constructor validates
and forwards them to __new__ / __init__. We therefore avoid validating the raw right-hand side
of member declarations in this case:
from enum import EnumMeta, IntEnum
class ChoicesType(EnumMeta):
def __new__(metacls, classname, bases, classdict, **kwds): ...
class IntegerChoices(IntEnum, metaclass=ChoicesType):
pass
class MyModelChoices(IntegerChoices):
GOOD = 1, "I like this"
reveal_type(MyModelChoices.GOOD.value) # revealed: Any
reveal_type(MyModelChoices.GOOD.name) # revealed: Literal["GOOD"]
An explicit _value_ annotation on the transformed enum class still takes precedence, even when the
annotation is inherited from a user-defined enum base:
from enum import EnumMeta, IntEnum
class ChoicesType(EnumMeta):
def __new__(metacls, classname, bases, classdict, **kwds): ...
class IntegerChoices(IntEnum, metaclass=ChoicesType):
_value_: int
class AnnotatedChoices(IntegerChoices):
GOOD = 1, "I like this"
reveal_type(AnnotatedChoices.GOOD.value) # revealed: int
reveal_type(AnnotatedChoices.GOOD._value_) # revealed: int
The metaclass can also transform assignments through __prepare__ or through an assigned __new__
hook:
from enum import EnumMeta, IntEnum
from typing import Any
class PreparedChoicesType(EnumMeta):
@classmethod
def __prepare__(metacls, cls: str, bases: tuple[type, ...], **kwds: Any) -> Any: ...
class PreparedChoices(IntEnum, metaclass=PreparedChoicesType):
GOOD = 1, "I like this"
reveal_type(PreparedChoices.GOOD.value) # revealed: Any
def external_metaclass_new(*args: Any, **kwargs: Any) -> Any: ...
class AssignedChoicesType(EnumMeta):
__new__ = staticmethod(external_metaclass_new)
class AssignedChoices(IntEnum, metaclass=AssignedChoicesType):
GOOD = 1, "I like this"
reveal_type(AssignedChoices.GOOD.value) # revealed: Any
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._internal 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.propertyEnum 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, assert_type
from ty_extensions._internal import enum_members
class Answer(Enum):
YES = 1
NO = 2
@enum_property
def some_property(self) -> str:
return "property value"
@enum_property
def settable_property(self) -> int:
return 1
@settable_property.setter
def settable_property(self, value: str) -> None:
pass
def direct_property_getter(self) -> int:
return 1
direct_property = enum_property(fget=direct_property_getter)
# revealed: tuple[Literal["YES"], Literal["NO"]]
reveal_type(enum_members(Answer))
assert_type(Answer.YES.some_property, str)
assert_type(Answer.YES.direct_property, int)
Answer.YES.some_property = "new value" # error: [invalid-assignment]
Answer.YES.settable_property = "new value"
assert_type(Answer.YES.settable_property, int)
Answer.YES.settable_property = 1 # error: [invalid-assignment]
def get(value: Enum) -> str:
return value.name
descriptor = enum_property(get)
reveal_type(descriptor) # revealed: enum.property
# revealed: <method-wrapper '__get__' of enum.property 'get'>
reveal_type(descriptor.__get__)
retained: enum_property = descriptor
retained_as_property: property = descriptor
not_enum_property: enum_property = property(get) # error: [invalid-assignment]
retained_getter: enum_property = descriptor.getter(get)
assert_type(descriptor.name, str)
assert_type(descriptor.clsname, str)
assert_type(descriptor.member, Enum | None)
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: ...
reveal_type(Choices.A.value) # revealed: Any
class BaseChoices(Enum):
@enum_property
def value(self) -> str:
return "custom value"
class InheritedChoices(BaseChoices):
A = 1
reveal_type(InheritedChoices.A.value) # revealed: str
types.DynamicClassAttributeAttributes defined using types.DynamicClassAttribute are not considered members:
from enum import Enum
from ty_extensions._internal 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))
Stubs can optionally use ... for the actual value:
from enum import Enum
from ty_extensions._internal 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))
Enum members can have aliases, which are not considered separate members:
from enum import Enum
from ty_extensions._internal 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)
Literal metadata does not affect aliasing at runtime. A value returned from a function is therefore still an alias of the same literal written directly in the class body:
from enum import Enum
from typing import Literal
from ty_extensions._internal import enum_members
def make_alias_value() -> Literal["value"]:
return "value"
class RuntimeAlias(Enum):
FIRST = make_alias_value()
SECOND = "value"
# revealed: tuple[Literal["FIRST"]]
reveal_type(enum_members(RuntimeAlias))
reveal_type(RuntimeAlias.SECOND) # revealed: RuntimeAlias
Multiple aliases to the same member are also supported. This is a regression test for https://github.com/astral-sh/ty/issues/1293:
from ty_extensions._internal 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"]
auto() still uses the preceding concrete value even when 1 and True compare equal:
from enum import Enum, auto
from ty_extensions._internal import enum_members
class IntThenTrue(Enum):
A = 1
B = True
C = auto()
# revealed: tuple[Literal["A"], Literal["B"], Literal["C"]]
reveal_type(enum_members(IntThenTrue))
reveal_type(IntThenTrue.C.value) # revealed: Literal[2]
Functional enums also detect duplicate-value aliases in both dict and list-of-tuples forms:
from enum import Enum
from ty_extensions._internal import enum_members
DictAlias = Enum("DictAlias", {"A": 1, "B": 1})
# revealed: tuple[Literal["A"]]
reveal_type(enum_members(DictAlias))
# single-member enum is a singleton, so member access resolves to the instance type
reveal_type(DictAlias.A) # revealed: DictAlias
reveal_type(DictAlias.B) # revealed: DictAlias
PairsAlias = Enum("PairsAlias", [("A", 1), ("B", 1)])
# revealed: tuple[Literal["A"]]
reveal_type(enum_members(PairsAlias))
reveal_type(PairsAlias.A) # revealed: PairsAlias
reveal_type(PairsAlias.B) # revealed: PairsAlias
auto()[environment]
python-version = "3.11"
from enum import Enum, auto
from ty_extensions._internal 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]
If auto() follows a non-literal value, the generated value widens to int since the previous
value isn't known at type-check time:
def f(n: int):
class StaticDynamic(Enum):
A = n
B = auto()
reveal_type(StaticDynamic.A.value) # revealed: int
reveal_type(StaticDynamic.B.value) # revealed: int
Dynamic = Enum("Dynamic", {"A": n, "B": auto()})
reveal_type(Dynamic.A.value) # revealed: int
reveal_type(Dynamic.B.value) # revealed: int
Bool literals are still concrete predecessors for auto():
class AfterFalse(Enum):
A = False
B = auto()
reveal_type(AfterFalse.B.value) # revealed: Literal[1]
class AfterTrue(Enum):
A = True
B = auto()
reveal_type(AfterTrue.B.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 produces precise literal values:
from enum import IntEnum, auto
class Answer(IntEnum):
YES = auto()
NO = auto()
reveal_type(Answer.YES.value) # revealed: Literal[1]
reveal_type(Answer.NO.value) # revealed: Literal[2]
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]
For an enum with a str data type, the generated value is still normalized to str. The result of
using auto() with other non-integer data types is
hard to predict, so we use
typeshed's Any annotation for .value in those cases:
from enum import Enum, auto
from typing import Any
class A(str, Enum):
X = auto()
Y = auto()
reveal_type(A.X.value) # revealed: str
class B(bytes, Enum):
X = auto()
Y = auto()
reveal_type(B.X.value) # revealed: Any
class C(tuple[Any, ...], 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
When _generate_next_value_ is overridden, its return type is used for auto() value types, unless
overridden by an explicit _value_ annotation or a custom construction hook:
from enum import StrEnum, IntEnum, auto
from typing import Literal
class CustomNextValue(Enum):
@staticmethod
def _generate_next_value_(name, start, count, last_values): ...
A = auto()
B = auto()
reveal_type(CustomNextValue.A.value) # revealed: Unknown
class CustomNextValueNonAuto(Enum):
@staticmethod
def _generate_next_value_(name, start, count, last_values) -> Literal[3]:
return 3
A = 1
B = 2
reveal_type(CustomNextValueNonAuto.A.value) # revealed: Literal[1]
class CustomNextValueStr(Enum):
@staticmethod
def _generate_next_value_(name, start, count, last_values) -> str:
return ""
A = auto()
B = auto()
# Should not be `Literal['A']`
# revealed: str
reveal_type(CustomNextValueStr.A.value)
class CustomNextValuePrecedence(Enum):
_value_: str
@staticmethod
def _generate_next_value_(name, start, count, last_values) -> Literal["a"]:
return "a"
A = auto()
B = auto()
# `_value_` annotation takes precedence over `_generate_next_value_`'s return type
# revealed: str
reveal_type(CustomNextValuePrecedence.A.value)
def foo(a: CustomNextValuePrecedence):
# revealed: str
reveal_type(a.value)
class CustomNextValueInt(IntEnum):
@staticmethod
def _generate_next_value_(name, start, count, last_values) -> Literal[42]:
return 42
A = auto()
B = auto()
# `IntEnum` inherits `_value_: int`, which takes precedence over `_generate_next_value_`
# revealed: int
reveal_type(CustomNextValueInt.A.value)
Assigning to _generate_next_value_ can prevent us from inspecting its return type, in which case,
auto() values become Any while explicit member values remain precise. We also avoid inferring
aliases between generated values:
from enum import Enum, auto
from ty_extensions._internal import enum_members
from typing import Any, Literal, cast
def external_generate_next_value(*args: Any) -> Any: ...
class ReassignedGenerator(Enum):
@staticmethod
def _generate_next_value_(name, start, count, last_values) -> Literal["same"]:
return "same"
_generate_next_value_ = cast(Any, staticmethod(external_generate_next_value))
A = auto()
B = auto()
EXPLICIT = 1
reveal_type(ReassignedGenerator.A.value) # revealed: Any
reveal_type(ReassignedGenerator.EXPLICIT.value) # revealed: Literal[1]
# revealed: tuple[Literal["A"], Literal["B"], Literal["EXPLICIT"]]
reveal_type(enum_members(ReassignedGenerator))
An assigned generator also shadows a function generator from a class later in the MRO:
class FunctionGeneratorBase(Enum):
@staticmethod
def _generate_next_value_(name, start, count, last_values) -> Literal["same"]:
return "same"
class AssignedGeneratorMiddle(FunctionGeneratorBase):
_generate_next_value_ = staticmethod(external_generate_next_value)
class AssignedGeneratorChild(AssignedGeneratorMiddle):
A = auto()
reveal_type(AssignedGeneratorChild.A.value) # revealed: Any
When an enum defines both _generate_next_value_ and a construction hook (__new__, __init__, or
a custom enum metaclass __new__), the hook can rewrite _value_ to a different type than the
value returned by _generate_next_value_. The hook-based Any fallback should therefore take
precedence:
from enum import Enum, EnumMeta, IntEnum, auto
from ty_extensions._internal import enum_members
from typing import Literal
class WithNewAndGenerateNextValue(Enum):
@staticmethod
def _generate_next_value_(name, start, count, last_values) -> str:
return ""
def __new__(cls, value: str) -> "WithNewAndGenerateNextValue":
obj = object.__new__(cls)
obj._value_ = len(value)
return obj
A = auto()
B = auto()
# `__new__` rewrites `_value_` to an `int`, so we can't trust `_generate_next_value_`'s return type
reveal_type(WithNewAndGenerateNextValue.A.value) # revealed: Any
def _instance_new(a: WithNewAndGenerateNextValue):
reveal_type(a.value) # revealed: Any
class WithNewAndLiteralGenerateNextValue(Enum):
@staticmethod
def _generate_next_value_(name, start, count, last_values) -> Literal["x"]:
return "x"
def __new__(cls, value: str) -> "WithNewAndLiteralGenerateNextValue":
obj = object.__new__(cls)
obj._value_ = object()
return obj
A = auto()
B = auto()
# `__new__` can rewrite duplicate generated values to distinct values, so `B` is not an alias of `A`.
# revealed: tuple[Literal["A"], Literal["B"]]
reveal_type(enum_members(WithNewAndLiteralGenerateNextValue))
reveal_type(WithNewAndLiteralGenerateNextValue.A) # revealed: Literal[WithNewAndLiteralGenerateNextValue.A]
reveal_type(WithNewAndLiteralGenerateNextValue.B) # revealed: Literal[WithNewAndLiteralGenerateNextValue.B]
reveal_type(WithNewAndLiteralGenerateNextValue.A.value) # revealed: Any
reveal_type(WithNewAndLiteralGenerateNextValue.B.value) # revealed: Any
class WithInitAndGenerateNextValue(Enum):
@staticmethod
def _generate_next_value_(name, start, count, last_values) -> str:
return ""
def __init__(self, value: str) -> None: ...
A = auto()
B = auto()
reveal_type(WithInitAndGenerateNextValue.A.value) # revealed: Any
def _instance_init(a: WithInitAndGenerateNextValue):
reveal_type(a.value) # revealed: Any
class WithInitAndLiteralGenerateNextValue(Enum):
@staticmethod
def _generate_next_value_(name, start, count, last_values) -> Literal["x"]:
return "x"
def __init__(self, value: str) -> None: ...
A = auto()
B = auto()
# `__init__` runs after duplicate generated values are resolved to aliases.
# revealed: tuple[Literal["A"]]
reveal_type(enum_members(WithInitAndLiteralGenerateNextValue))
class ChoicesType(EnumMeta):
def __new__(metacls, classname, bases, classdict, **kwds): ...
class IntegerChoices(IntEnum, metaclass=ChoicesType):
@staticmethod
def _generate_next_value_(name, start, count, last_values) -> Literal[42]:
return 42
class MyModelChoices(IntegerChoices):
A = auto()
B = auto()
# The metaclass `__new__` can rewrite member values before they reach `_value_`
reveal_type(MyModelChoices.A.value) # revealed: Any
def _instance_metaclass(a: MyModelChoices):
reveal_type(a.value) # revealed: Any
class IntEnumDuplicateAutoAliases(IntEnum):
@staticmethod
def _generate_next_value_(name, start, count, last_values) -> Literal[42]:
return 42
A = auto()
B = auto()
# The stdlib `IntEnum.__new__` preserves duplicate generated values as aliases.
# revealed: tuple[Literal["A"]]
reveal_type(enum_members(IntEnumDuplicateAutoAliases))
For non-auto() members in a mixed enum, _generate_next_value_ does not apply at all, and the
inferred value type should be used (subject to the same hook-based Any fallback):
from enum import Enum, auto
from ty_extensions._internal import enum_members
from typing import Literal
class MixedAutoAndLiteral(Enum):
@staticmethod
def _generate_next_value_(name, start, count, last_values) -> str:
return ""
A = auto()
B = 99
reveal_type(MixedAutoAndLiteral.A.value) # revealed: str
reveal_type(MixedAutoAndLiteral.B.value) # revealed: Literal[99]
def _mixed_instance(x: MixedAutoAndLiteral):
# Union of all member value types, not just `_generate_next_value_`'s return type
reveal_type(x.value) # revealed: str | Literal[99]
class InheritedCustomNextValue(Enum):
@staticmethod
def _generate_next_value_(name, start, count, last_values) -> str:
return ""
class InheritedCustomNextValueChild(InheritedCustomNextValue):
A = auto()
B = 1
C = 1
# `A` uses the inherited `_generate_next_value_`, so `B` is not an alias of `A`.
# revealed: tuple[Literal["A"], Literal["B"]]
reveal_type(enum_members(InheritedCustomNextValueChild))
reveal_type(InheritedCustomNextValueChild.A.value) # revealed: str
reveal_type(InheritedCustomNextValueChild.B) # revealed: Literal[InheritedCustomNextValueChild.B]
reveal_type(InheritedCustomNextValueChild.B.value) # revealed: Literal[1]
reveal_type(InheritedCustomNextValueChild.C) # revealed: Literal[InheritedCustomNextValueChild.B]
def _inherited_mixed_instance(x: InheritedCustomNextValueChild):
reveal_type(x.value) # revealed: str | Literal[1]
auto() after an aliasEven when a declaration becomes an alias, its original value is included in the last_values passed
to _generate_next_value_. Here, TRUE is an alias of ONE, but AFTER still receives the value
2:
from enum import Enum, auto
from ty_extensions._internal import enum_members
class Mixed(int, Enum):
ONE = 1
TRUE = True
AFTER = auto()
reveal_type(Mixed.AFTER.value) # revealed: Literal[2]
# revealed: tuple[Literal["ONE"], Literal["AFTER"]]
reveal_type(enum_members(Mixed))
member and nonmember[environment]
python-version = "3.11"
from enum import Enum, auto, member, nonmember
from ty_extensions._internal 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._internal 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))
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._internal 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))
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._internal 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))
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._internal 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]
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 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
from enum import Enum
class Answer(Enum):
YES = 1
NO = 2
def through_self(self) -> None:
reveal_type(self.YES) # revealed: Literal[Answer.YES]
reveal_type(self.NO) # revealed: Literal[Answer.NO]
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]
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]
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!")
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"]
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._internal 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))
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]
def narrowed_meta_type(answer: Answer):
if answer is Answer.YES:
return
reveal_type(type(answer)) # revealed: <class 'Answer'>
reveal_type(answer.__class__) # revealed: <class 'Answer'>
from enum import Enum
from typing import Literal
from ty_extensions._internal 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))
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.
Enumfrom 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
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]
reveal_type(EnumWithEnumMetaMetaclass(0)) # revealed: EnumWithEnumMetaMetaclass
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"]
Open EnumMeta-based classes still reject ordinary calls until they are finalized with members:
from enum import EnumMeta
class Meta(EnumMeta): ...
class Empty(metaclass=Meta): ...
# error: [too-many-positional-arguments]
Empty(1)
EnumType as metaclassIn Python 3.11, the meta-type was renamed to EnumType.
[environment]
python-version = "3.11"
On Python 3.11+, open EnumMeta-based classes also accept the functional-enum calling convention,
though the inferred result is still imprecise:
from enum import EnumMeta
class Meta(EnumMeta): ...
class Empty(metaclass=Meta): ...
# TODO: runtime MRO suggests this should be closer to `type[Empty]`.
reveal_type(Empty("Dynamic", {"X": 1})) # revealed: type[Enum]
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"]
from enum import Enum
from ty_extensions._internal 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))
from enum import Enum
from ty_extensions._internal import enum_members
Color = Enum("Color", names="RED GREEN BLUE")
# revealed: tuple[Literal["RED"], Literal["GREEN"], Literal["BLUE"]]
reveal_type(enum_members(Color))
The name passed to Enum must match the variable it is assigned to:
from enum import Enum
GoodMatch1 = Enum("GoodMatch1", "A B") # fine
name = "GoodMatch2"
GoodMatch2 = Enum(name, "A B") # also fine
If there is a mismatch, we emit the following diagnostic:
# snapshot: mismatched-type-name
Mismatch = Enum("WrongName", "A B")
warning[mismatched-type-name]: The name passed to `Enum` must match the variable it is assigned to
--> src/mdtest_snippet.py:8:17
|
8 | Mismatch = Enum("WrongName", "A B")
| ^^^^^^^^^^^ Expected "Mismatch", got "WrongName"
|
If the name is not a string literal, we also emit a diagnostic:
def f(name: str) -> None:
# snapshot: mismatched-type-name
DynamicMismatch = Enum(name, "A B")
warning[mismatched-type-name]: The name passed to `Enum` must match the variable it is assigned to
--> src/mdtest_snippet.py:11:28
|
11 | DynamicMismatch = Enum(name, "A B")
| ^^^^ Expected "DynamicMismatch", got variable of type `str`
|
from enum import Enum
from ty_extensions._internal 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))
from enum import Enum
from ty_extensions._internal import enum_members
Color = Enum("Color", ["RED", "GREEN", "BLUE"])
# revealed: tuple[Literal["RED"], Literal["GREEN"], Literal["BLUE"]]
reveal_type(enum_members(Color))
from enum import Enum
from ty_extensions._internal 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]
auto()from enum import Enum, auto
from ty_extensions._internal 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._internal 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]
This also applies when the previous value is a bool literal:
from enum import Enum, auto
AfterFalse = Enum("AfterFalse", {"A": False, "B": auto()})
reveal_type(AfterFalse.B.value) # revealed: Literal[1]
AfterTrue = Enum("AfterTrue", {"A": True, "B": auto()})
reveal_type(AfterTrue.B.value) # revealed: Literal[2]
auto() in tuple/list entriesauto() should also expand in tuple/list entry forms of the functional syntax:
from enum import Enum, Flag, auto
Color = Enum("Color", [("RED", auto()), ("GREEN", auto())])
reveal_type(Color.RED.value) # revealed: Literal[1]
reveal_type(Color.GREEN.value) # revealed: Literal[2]
Perm = Flag("Perm", (("READ", auto()), ("WRITE", auto())))
reveal_type(Perm.READ.value) # revealed: Literal[1]
reveal_type(Perm.WRITE.value) # revealed: Literal[2]
Explicit-value forms should ignore start, just like static enums do:
from enum import Enum, Flag, auto
Color = Enum("Color", [("RED", auto()), ("GREEN", auto())], start=3)
reveal_type(Color.RED.value) # revealed: Literal[1]
reveal_type(Color.GREEN.value) # revealed: Literal[2]
Mapped = Enum("Mapped", {"RED": auto(), "GREEN": auto()}, start=3)
reveal_type(Mapped.RED.value) # revealed: Literal[1]
reveal_type(Mapped.GREEN.value) # revealed: Literal[2]
Perm = Flag("Perm", (("READ", auto()), ("WRITE", auto())), start=3)
reveal_type(Perm.READ.value) # revealed: Literal[1]
reveal_type(Perm.WRITE.value) # revealed: Literal[2]
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._internal 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
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
from ty_extensions._internal import enum_members
def f(
names: list[str],
labels: str,
pairs: tuple[tuple[str, int], ...],
mapping: dict[str, int],
name: str,
key: str,
) -> None:
E1 = Enum("E1", names)
E2 = Enum("E2", labels)
E3 = Enum("E3", pairs)
E4 = Enum("E4", mapping)
E5 = Enum("E5", ["A", name])
E6 = Enum("E6", [(name, 1)])
E7 = Enum("E7", {key: 1})
reveal_type(enum_members(E1)) # revealed: Unknown
reveal_type(enum_members(E2)) # revealed: Unknown
reveal_type(enum_members(E3)) # revealed: Unknown
reveal_type(enum_members(E4)) # revealed: Unknown
reveal_type(enum_members(E5)) # revealed: Unknown
reveal_type(enum_members(E6)) # revealed: Unknown
reveal_type(enum_members(E7)) # revealed: Unknown
# Inherited class attributes resolve from Enum base.
reveal_type(E1.__members__) # revealed: MappingProxyType[str, E1]
# But own member access is unknown.
reveal_type(E1.FOO) # revealed: Unknown
Enum(value, names, *, ...) only accepts two positional args at runtime.
from enum import Enum
from ty_extensions._internal import enum_members
# error: [too-many-positional-arguments]
Color = Enum("Color", "RED", "GREEN", "BLUE")
reveal_type(enum_members(Color)) # revealed: Unknown
Passing the same functional-enum parameter both positionally and by keyword should still report the usual duplicate-argument diagnostic:
from enum import Enum
from ty_extensions._internal import enum_members
# error: [parameter-already-assigned]
Color = Enum("Color", "RED", names="BLUE")
reveal_type(enum_members(Color)) # revealed: Unknown
from enum import Enum
# This is invalid at runtime but should not panic.
Color = Enum()
reveal_type(Color) # revealed: Enum
names argumentfrom enum import Enum
# This is invalid at runtime but should not panic.
Enum("Color") # error: [missing-argument]
# This is invalid at runtime but should not panic.
Enum(value="Color") # error: [missing-argument]
# error: [missing-argument]
# error: [invalid-argument-type]
Enum(123)
# error: [missing-argument]
# error: [invalid-argument-type]
Enum(value=123)
# error: [missing-argument]
# error: [invalid-argument-type]
Enum("Color", start="0")
# error: [missing-argument]
# error: [invalid-argument-type]
Enum("Color", type=1)
# error: [missing-argument]
# error: [unknown-argument]
Enum("Color", bad_kwarg=True)
Non-literal names should still be recognized as creating an enum class.
from enum import Enum
def make_enum(name: str) -> type[Enum]:
# error: [mismatched-type-name]
result = Enum(name.title(), "RED BLUE", module=__name__)
reveal_type(result) # revealed: type[Enum]
return result
def validate_other_args(name: str) -> None:
# error: [invalid-argument-type]
Enum(name, "RED", start="0")
# error: [invalid-argument-type]
Enum(name, "RED", type=1)
from enum import Enum
# error: [invalid-argument-type]
Color = Enum(123, "RED GREEN BLUE")
from enum import Enum
# error: [unknown-argument]
Color = Enum("Color", "RED GREEN BLUE", bad_kwarg=True)
names argumentsFunctional enums should still reject obviously invalid names values:
from enum import Enum
from ty_extensions._internal import enum_members
# error: [invalid-argument-type]
Color = Enum("Color", 123)
reveal_type(enum_members(Color)) # revealed: Unknown
Empty functional enums are valid, even though they have no members:
from enum import Enum
from ty_extensions._internal import enum_members
EmptyFromString = Enum("EmptyFromString", "")
EmptyFromList = Enum("EmptyFromList", [])
EmptyFromDict = Enum("EmptyFromDict", {})
reveal_type(enum_members(EmptyFromString)) # revealed: tuple[()]
reveal_type(enum_members(EmptyFromList)) # revealed: tuple[()]
reveal_type(enum_members(EmptyFromDict)) # revealed: tuple[()]
class ExtendedEmpty(EmptyFromString):
A = 1
# revealed: tuple[Literal["A"]]
reveal_type(enum_members(ExtendedEmpty))
Literal list/tuple/dict inputs that use unpacking are rejected:
from enum import Enum
names: list[str] = ["B"]
pairs: list[tuple[str, int]] = [("B", 2)]
more: dict[str, int] = {"B": 2}
bad_keys: dict[int, int] = {1: 2}
# error: [invalid-argument-type]
Enum("FromNames", ["A", *names])
# error: [invalid-argument-type]
Enum("FromPairs", [("A", 1), *pairs])
# error: [invalid-argument-type]
Enum("FromMapping", {"A": 1, **more})
# error: [invalid-argument-type]
Enum("BadDoubleStar", {**bad_keys})
Functional enum construction should still preserve overload-based argument validation:
from enum import Enum
# error: [invalid-argument-type]
Color = Enum("Color", "RED", start="0")
reveal_type(Color.RED.value) # revealed: Literal[1]
boundary keyword (Python 3.11+)[environment]
python-version = "3.11"
from enum import Flag
Perm = Flag("Perm", "READ WRITE EXECUTE", boundary=None)
[environment]
python-version = "3.10"
from enum import Flag
# error: [unknown-argument]
Perm = Flag("Perm", "READ WRITE EXECUTE", boundary=None)
[environment]
python-version = "3.11"
from enum import StrEnum
from ty_extensions._internal 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"]
from enum import Enum, Flag
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]
Perm = Flag("Perm", "READ WRITE EXECUTE", start=3)
reveal_type(Perm.READ.value) # revealed: Literal[3]
reveal_type(Perm.WRITE.value) # revealed: Literal[4]
reveal_type(Perm.EXECUTE.value) # revealed: Literal[8]
Non-literal integer start values should widen member values to int rather than pretending the
default start=1 was used:
from enum import Enum, Flag
def make(n: int) -> None:
Color = Enum("Color", "RED GREEN", start=n)
reveal_type(Color.RED.value) # revealed: int
reveal_type(Color.GREEN.value) # revealed: int
Perm = Flag("Perm", "READ WRITE", start=n)
reveal_type(Perm.READ.value) # revealed: int
reveal_type(Perm.WRITE.value) # revealed: int
from enum import Enum, auto
from ty_extensions._internal import enum_members
Http = Enum("Http", "OK NOT_FOUND", type=int)
reveal_type(Http.OK) # revealed: Literal[Http.OK]
reveal_type(Http.OK.value) # revealed: Literal[1]
reveal_type(Http.NOT_FOUND.value) # revealed: Literal[2]
# revealed: tuple[Literal["OK"], Literal["NOT_FOUND"]]
reveal_type(enum_members(Http))
StringyNames = Enum("StringyNames", "A B", type=str)
BytesyNames = Enum("BytesyNames", "A B", type=bytes)
FloatyNames = Enum("FloatyNames", "A B", type=float)
reveal_type(StringyNames.A.value) # revealed: Literal["1"]
reveal_type(StringyNames.B.value) # revealed: Literal["2"]
reveal_type(BytesyNames.A.value) # revealed: bytes
reveal_type(BytesyNames.B.value) # revealed: bytes
reveal_type(FloatyNames.A.value) # revealed: float
reveal_type(FloatyNames.B.value) # revealed: float
# revealed: tuple[Literal["A"], Literal["B"]]
reveal_type(enum_members(StringyNames))
# revealed: tuple[Literal["A"], Literal["B"]]
reveal_type(enum_members(BytesyNames))
# revealed: tuple[Literal["A"], Literal["B"]]
reveal_type(enum_members(FloatyNames))
Parsed = Enum("Parsed", {"A": "1"}, type=int)
Stringy = Enum("Stringy", {"A": "1", "B": auto()}, type=str)
reveal_type(enum_members(Parsed)) # revealed: Unknown
reveal_type(enum_members(Stringy)) # revealed: Unknown
class Prefixed(str):
pass
CustomNames = Enum("CustomNames", "A B", type=Prefixed)
Custom = Enum("Custom", {"A": "1"}, type=Prefixed)
reveal_type(enum_members(CustomNames)) # revealed: Unknown
reveal_type(enum_members(Custom)) # revealed: Unknown
Functional enums should still validate type= arguments eagerly, both for obvious non-types and for
bases that are structurally invalid to combine with Enum:
from enum import Enum
from typing import TypedDict
from ty_extensions._internal import reveal_mro
# error: [invalid-argument-type]
BadType = Enum("BadType", "RED", type=1)
# error: [invalid-argument-type]
BadStringType = Enum("BadStringType", "RED", type="Mixin")
TD = TypedDict("TD", {"x": int})
# error: [invalid-base]
BadBase = Enum("BadBase", "RED", type=TD)
reveal_mro(BadBase) # revealed: (<class 'BadBase'>, <class 'Enum'>, <class 'object'>)
Mixins that are incompatible with the enum base should still report an error and avoid exposing a precise member set:
from enum import IntEnum, IntFlag
from ty_extensions._internal import enum_members
# error: [invalid-base]
BadIntEnum = IntEnum("BadIntEnum", "RED", type=str)
# error: [invalid-base]
BadIntFlag = IntFlag("BadIntFlag", "RED", type=float)
reveal_type(enum_members(BadIntEnum)) # revealed: Unknown
reveal_type(enum_members(BadIntFlag)) # revealed: Unknown
Functional enums with a type= mixin should also have the same MRO as the equivalent static enum
class:
from enum import Enum
from ty_extensions._internal import reveal_mro
Http = Enum("Http", "OK NOT_FOUND", type=int)
reveal_mro(Http) # revealed: (<class 'Http'>, <class 'int'>, <class 'Enum'>, <class 'object'>)
class StaticHttp(int, Enum):
OK = 1
NOT_FOUND = 2
reveal_mro(StaticHttp) # revealed: (<class 'StaticHttp'>, <class 'int'>, <class 'Enum'>, <class 'object'>)
from enum import IntEnum
from ty_extensions._internal import enum_members
Color = IntEnum("Color", "RED GREEN BLUE")
# revealed: tuple[Literal["RED"], Literal["GREEN"], Literal["BLUE"]]
reveal_type(enum_members(Color))
Number = IntEnum("Number", {"FALSE": False, "ZERO": 0})
reveal_type(Number.FALSE.value) # revealed: Literal[0]
reveal_type(Number.ZERO) # revealed: Number
reveal_type(enum_members(Number)) # revealed: tuple[Literal["FALSE"]]
# `int("1")` widens to `int`, but identical raw values still prove that the
# members are aliases.
Parsed = IntEnum("Parsed", {"A": "1", "B": "1"})
reveal_type(Parsed.B) # revealed: Parsed
reveal_type(enum_members(Parsed)) # revealed: tuple[Literal["A"]]
from enum import Flag
from ty_extensions._internal 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]
from enum import IntFlag
from ty_extensions._internal 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]
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
from enum import Enum
Answer = Enum("Answer", "YES NO")
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]
type[…]from enum import Enum
Answer = Enum("Answer", "YES NO")
def _(answer: type[Answer]) -> None:
reveal_type(answer.YES) # revealed: Literal[Answer.YES]
reveal_type(answer.NO) # revealed: Literal[Answer.NO]
Functional enums with members should also be implicitly final:
from enum import Enum
Color = Enum("Color", "RED GREEN BLUE")
# error: [subclass-of-final-class]
class ExtendedColor(Color):
YELLOW = 4
from enum import Enum
Answer = Enum("Answer", "YES NO")
reveal_type(type(Answer.YES)) # revealed: <class 'Answer'>
def _(answer: Answer):
reveal_type(type(answer)) # revealed: <class 'Answer'>
if statementsfrom enum import Enum
from typing import Any, Protocol, TypeVar
from typing_extensions import Literal, assert_never, assert_type, overload
from ty_extensions import Intersection
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_value_without_red(color: Color) -> Literal[2, 3]:
if color is Color.RED:
raise ValueError()
reveal_type(color.value) # revealed: Literal[2, 3]
return color.value
def color_value_without_red_and_with_any(color: Intersection[Color, Any]) -> Literal[2, 3]:
if color is Color.RED:
raise ValueError()
reveal_type(color) # revealed: Color & Any & ~Literal[Color.RED]
reveal_type(color.value) # revealed: Literal[2, 3]
return color.value
T = TypeVar("T")
def color_value_without_red_and_with_typevar(
color: Intersection[Color, T],
) -> Literal[2, 3]:
if color is Color.RED:
raise ValueError()
reveal_type(color.value) # revealed: Literal[2, 3]
return color.value
class HasValue(Protocol):
@property
def value(self) -> Literal[2]: ...
TWithRestrictedValue = TypeVar("TWithRestrictedValue", bound=HasValue)
def color_value_without_red_and_with_restricted_typevar(
color: Intersection[Color, TWithRestrictedValue],
) -> Literal[2]:
if color is Color.RED:
raise ValueError()
reveal_type(color.value) # revealed: Literal[2]
return color.value
def color_truthy_without_red(color: Color) -> int:
if color is Color.RED:
raise ValueError()
if color:
return 1
def color_after_merge(color: Color) -> None:
if color is Color.RED:
merged = color
else:
merged = color
reveal_type(merged) # revealed: Color
assert_type(merged, Color)
def color_after_different_complement_merge(color: Color, flag: bool) -> None:
if flag:
if color is Color.RED:
return
merged = color
else:
if color is Color.GREEN:
return
merged = color
reveal_type(merged) # revealed: Color
assert_type(merged, Color)
def color_after_grouped_literal_and_complement_merge(color: Color, flag: bool, other_flag: bool) -> None:
if flag:
if other_flag:
merged = Color.BLUE
else:
merged = Color.RED
else:
if color is Color.RED:
return
merged = color
reveal_type(merged) # revealed: Color
assert_type(merged, Color)
def color_after_dynamic_complement_merge(color: Intersection[Color, Any], flag: bool) -> None:
if flag:
if color is Color.RED:
return
merged = color
else:
if color is Color.GREEN:
return
merged = color
reveal_type(merged) # revealed: Color & Any
assert_type(merged, Intersection[Color, Any])
def color_after_shared_complement_merge(color: Color, flag: bool) -> None:
if color is Color.RED:
return
if flag:
merged = color
else:
if color is Color.GREEN:
return
merged = color
reveal_type(merged) # revealed: Literal[Color.GREEN, Color.BLUE]
assert_type(merged, Literal[Color.GREEN, Color.BLUE])
def color_compare_without_red(color: Color) -> None:
if color is Color.RED:
return
reveal_type(color == Color.RED) # revealed: Literal[False]
reveal_type(color != Color.RED) # revealed: Literal[True]
@overload
def color_overload(color: Literal[Color.GREEN]) -> Literal["green"]: ...
@overload
def color_overload(color: Literal[Color.BLUE]) -> Literal["blue"]: ...
def color_overload(color: Color) -> str:
return color.name.lower()
def color_overload_without_red(color: Color) -> None:
if color is Color.RED:
return
reveal_type(color_overload(color)) # revealed: Literal["green", "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, IntEnum
from typing_extensions import Literal, 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)
class ThreadSubset(IntEnum):
WARP = 1
WARPGROUP = 2
BLOCK = 3
def thread_subset_name(value: Literal[ThreadSubset.WARPGROUP, ThreadSubset.WARP]) -> str:
match value:
case ThreadSubset.WARPGROUP:
return "Warpgroup"
case ThreadSubset.WARP:
return "Warp"
case _:
assert_never(value)
class CustomEq(Enum):
A = 1
B = 2
def __eq__(self, other: object) -> bool:
return False
def custom_eq_match(value: Literal[CustomEq.A, CustomEq.B]) -> str:
match value:
case CustomEq.A:
return "A"
case CustomEq.B:
return "B"
case _:
reveal_type(value) # revealed: CustomEq
return "default"
if statements (function syntax)from enum import Enum
from typing_extensions import assert_never
Color = Enum("Color", "RED GREEN BLUE")
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)
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`"
match statements (function syntax)TODO: match exhaustiveness does not yet work for functional enums. The pattern matching narrowing
path does not resolve functional enum members the same way is comparisons do.
[environment]
python-version = "3.10"
from enum import Enum
from typing_extensions import assert_never
Color = Enum("Color", "RED GREEN BLUE")
# TODO: `assert_never` should not fire here (exhaustive match).
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) # error: [type-assertion-failure]
# TODO: This should ideally emit `Literal[Color.BLUE]` in the assertion, not `Color`.
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 `Color` is not equivalent to `Never`"
__eq__ and __ne____eq__ or __ne__ overridesfrom 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]
__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
__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
Enum classes cannot be generic. Python does not support generic enums, and attempting to create one
will result in a TypeError at runtime.
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
Generic base classInheriting 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
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
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
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
[environment]
python-version = "3.11"
The constructor of an enum takes a single value argument and returns the enum member corresponding
to that value:
from enum import Enum, IntEnum, StrEnum
from ty_extensions._internal import into_regular_callable
class Color(Enum):
RED = 1
BLUE = 2
# revealed: (value: object) -> Color
reveal_type(into_regular_callable(Color))
class Priority(IntEnum):
HIGH = 1
LOW = 2
# revealed: (value: int) -> Priority
reveal_type(into_regular_callable(Priority))
class Answer(StrEnum):
YES = "yes"
NO = "no"
# revealed: (value: str) -> Answer
reveal_type(into_regular_callable(Answer))
The signature of Enum, IntEnum, and StrEnum is defined by EnumMeta.__call__, which allows
dynamic construction of enums using the functional syntax:
from enum import Enum, IntEnum, StrEnum
from ty_extensions._internal import into_regular_callable
# revealed: Overload[[_EnumMemberT](value: Any, names: None = None) -> _EnumMemberT, (value: str, names: Iterable[Iterable[str | Any]], *, module: str | None = None, qualname: str | None = None, type: type | None = None, start: int = 1, boundary: FlagBoundary | None = None) -> type[Enum]]
reveal_type(into_regular_callable(Enum))
# revealed: Overload[[_EnumMemberT](value: Any, names: None = None) -> _EnumMemberT, (value: str, names: Iterable[Iterable[str | Any]], *, module: str | None = None, qualname: str | None = None, type: type | None = None, start: int = 1, boundary: FlagBoundary | None = None) -> type[Enum]]
reveal_type(into_regular_callable(IntEnum))
# revealed: Overload[[_EnumMemberT](value: Any, names: None = None) -> _EnumMemberT, (value: str, names: Iterable[Iterable[str | Any]], *, module: str | None = None, qualname: str | None = None, type: type | None = None, start: int = 1, boundary: FlagBoundary | None = None) -> type[Enum]]
reveal_type(into_regular_callable(StrEnum))