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
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 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 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 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))
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 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
_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
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.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
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.DynamicClassAttributeAttributes 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))
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))
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 https://github.com/astral-sh/ty/issues/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"]
auto() still uses the preceding concrete value even when 1 and True compare equal:
from enum import Enum, auto
from ty_extensions 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 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 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 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))
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))
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))
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]
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
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 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]
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))
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 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 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 mitmatch, 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 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 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 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 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]
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 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 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 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 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 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 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 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 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 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 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 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 import enum_members
Color = IntEnum("Color", "RED GREEN BLUE")
# revealed: tuple[Literal["RED"], Literal["GREEN"], Literal["BLUE"]]
reveal_type(enum_members(Color))
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]
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]
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_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)
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 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 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))