diff --git a/docs/spec/enums.rst b/docs/spec/enums.rst new file mode 100644 index 00000000..b9759e54 --- /dev/null +++ b/docs/spec/enums.rst @@ -0,0 +1,363 @@ +Enumerations +============ + +The ``enum.Enum`` class behaves differently from other Python classes in several +ways that require special-case handling in type checkers. This section discusses +the Enum behaviors that should be supported by type checkers and others which +may be supported optionally. It is recommended that library and type stub +authors avoid using optional behaviors because these may not be supported +by some type checkers. + + +Enum Definition +--------------- + +Enum classes can be defined using a "class syntax" or a "function syntax". +The function syntax offers several ways to specify enum members: names passed +as individual arguments, a list or tuple of names, a string of +comma-delimited or space-delimited names, a list or tuple of tuples that contain +name/value pairs, and a dictionary of name/value items. + +Type checkers should support the class syntax, but the function syntax (in +its various forms) is optional:: + + class Color1(Enum): # Supported + RED = 1 + GREEN = 2 + BLUE = 3 + + Color2 = Enum('Color2', 'RED', 'GREEN', 'BLUE') # Optional + Color3 = Enum('Color3', ['RED', 'GREEN', 'BLUE']) # Optional + Color4 = Enum('Color4', ('RED', 'GREEN', 'BLUE')) # Optional + Color5 = Enum('Color5', 'RED, GREEN, BLUE') # Optional + Color6 = Enum('Color6', 'RED GREEN BLUE') # Optional + Color7 = Enum('Color7', [('RED': 1), ('GREEN': 2), ('BLUE': 3)]) # Optional + Color8 = Enum('Color8', (('RED': 1), ('GREEN': 2), ('BLUE': 3))) # Optional + Color9 = Enum('Color9', {'RED': 1, 'GREEN': 2, 'BLUE': 3}) # Optional + +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. Note that +``enum.EnumType`` was named ``enum.EnumMeta`` prior to Python 3.11. Type +checkers should treat such classes as enums:: + + class CustomEnum1(Enum): + pass + + class Color7(CustomEnum1): # Supported + RED = 1 + GREEN = 2 + BLUE = 3 + + class CustomEnumType(EnumType): + pass + + class CustomEnum2(metaclass=CustomEnumType): + pass + + class Color8(CustomEnum2): # Supported + RED = 1 + GREEN = 2 + BLUE = 3 + + +Enum Behaviors +-------------- + +Enum classes are iterable and indexable, and they can be called with a value +to look up the enum member with that value. Type checkers should support these +behaviors:: + + class Color(Enum): + RED = 1 + GREEN = 2 + BLUE = 3 + + for color in Color: + reveal_type(color) # Revealed type is 'Color' + + reveal_type(Color["RED"]) # Revealed type is 'Literal[Color.RED]' (or 'Color') + reveal_type(Color(3)) # Revealed type is 'Literal[Color.BLUE]' (or 'Color') + +Unlike most Python classes, Calling an enum class does not invoke its constructor. +Instead, the call performs a value-based lookup of an enum member. + +An Enum class with one or more defined members cannot be subclassed. They are +implicitly "final". Type checkers should enforce this:: + + class EnumWithNoMembers(Enum): + pass + + class Shape(EnumWithNoMembers): # OK (because no members are defined) + SQUARE = 1 + CIRCLE = 2 + + class ExtendedShape(Shape): # Type checker error: Shape is implicitly final + TRIANGLE = 3 + + +Defining Members +---------------- + +When using the "class syntax", enum classes can define both members and +other (non-member) attributes. The ``EnumType`` metaclass applies a set +of rules to distinguish between members and non-members. Type checkers +should honor the most common of these rules. The lesser-used rules are +optional. Some of these rules may be impossible to evaluate and enforce +statically in cases where dynamic values are used. + +* If an attribute is defined in the class body with a type annotation but + with no assigned value, a type checker should assume this is a non-member + attribute:: + + class Pet(Enum): + genus: str # Non-member attribute + species: str # Non-member attribute + + CAT = 1 # Member attribute + DOG = 2 # Member attribute + + Within a type stub, members can be defined using the actual runtime values, + or a placeholder of ``...`` can be used:: + + class Pet(Enum): + genus: str # Non-member attribute + species: str # Non-member attribute + + CAT = ... # Member attribute + DOG = ... # Member attribute + +* Members defined within an enum class should not include explicit type + annotations. Type checkers should infer a literal type for all members. + A type checker should report an error if a type annotation is used + for an enum member because this type will be incorrect and misleading + to readers of the code:: + + class Pet(Enum): + CAT = 1 # OK + DOG: int = 2 # Type checker error + +* Methods, callables, descriptors (including properties), and nested classes + that are defined in the class are not treated as enum members by the + ``EnumType`` metaclass and should likewise not be treated as enum members by + a type checker:: + + def identity(x): return x + + class Pet(Enum): + CAT = 1 # Member attribute + DOG = 2 # Member attribute + + converter = lambda x: str(x) # Non-member attribute + transform = identity # Non-member attribute + + @property + def species(self) -> str: # Non-member property + return "mammal" + + def speak(self) -> None: # Non-member method + print("meow" if self is Pet.CAT else "woof") + + class Nested: ... # Non-member nested class + +* An attribute that is assigned the value of another member of the same enum + is not a member itself. Instead, it is an alias for the first member:: + + class TrafficLight(Enum): + RED = 1 + GREEN = 2 + YELLOW = 3 + + AMBER = YELLOW # Alias for YELLOW + + reveal_type(TrafficLight.AMBER) # Revealed type is Literal[TrafficLight.YELLOW] + +* If using Python 3.11 or newer, the ``enum.member`` and ``enum.nonmember`` + classes can be used to unambiguously distinguish members from non-members. + Type checkers should support these classes:: + + class Example(Enum): + a = member(1) # Member attribute + b = nonmember(2) # Non-member attribute + + @member + def c(self) -> None: # Member method + pass + + reveal_type(Example.a) # Revealed type is Literal[Example.a] + reveal_type(Example.b) # Revealed type is int or Literal[2] + reveal_type(Example.c) # Revealed type is Literal[Example.c] + +* An attribute with a private name (beginning with, but not ending in, a double + underscore) is treated as a non-member. + + class Example(Enum): + A = 1 # Member attribute + __B = 2 # Non-member attribute + + reveal_type(Example.A) # Revealed type is Literal[Example.A] + reveal_type(Example.__B) # Type Error: Private name is mangled + +* An enum class can define a class symbol named ``_ignore_``. This can be a list + of names or a string containing a space-delimited list of names that are + deleted from the enum class at runtime. Type checkers may support this + mechanism:: + + class Pet(Enum): + _ignore_ = "DOG FISH" + CAT = 1 # Member attribute + DOG = 2 # temporary variable, will be removed from the final enum class + FISH = 3 # temporary variable, will be removed from the final enum class + + +Member Names +------------ + +All enum member objects have an attribute ``_name_`` that contains the member's +name. They also have a property ``name`` that returns the same name. Type +checkers may infer a literal type for the name of a member:: + + class Color(Enum): + RED = 1 + GREEN = 2 + BLUE = 3 + + reveal_type(Color.RED._name_) # Revealed type is Literal["RED"] (or str) + reveal_type(Color.RED.name) # Revealed type is Literal["RED"] (or str) + + def func1(red_or_blue: Literal[Color.RED, Color.BLUE]): + reveal_type(red_or_blue.name) # Revealed type is Literal["RED", "BLUE"] (or str) + + def func2(any_color: Color): + reveal_type(any_color.name) # Revealed type is Literal["RED", "BLUE", "GREEN"] (or str) + + +Member Values +------------- + +All enum member objects have an attribute ``_value_`` that contains the member's +value. They also have a property ``value`` that returns the same value. Type +checkers may infer the type of a member's value:: + + class Color(Enum): + RED = 1 + GREEN = 2 + BLUE = 3 + + reveal_type(Color.RED._value_) # Revealed type is Literal[1] (or int or object or Any) + reveal_type(Color.RED.value) # Revealed type is Literal[1] (or int or object or Any) + + def func1(red_or_blue: Literal[Color.RED, Color.BLUE]): + reveal_type(red_or_blue.value) # Revealed type is Literal[1, 2] (or int or object or Any) + + def func2(any_color: Color): + reveal_type(any_color.value) # Revealed type is Literal[1, 2, 3] (or int or object or Any) + + +The value of ``_value_`` can be assigned in a constructor method. This technique +is sometimes used to initialize both the member value and non-member attributes. +If the value assigned in the class body is a tuple, the unpacked tuple value is +passed to the constructor. Type checkers may validate consistency between assigned +tuple values and the constructor signature:: + + class Planet(Enum): + def __init__(self, value: int, mass: float, radius: float): + self._value_ = value + self.mass = mass + self.radius = radius + + MERCURY = (1, 3.303e+23, 2.4397e6) + VENUS = (2, 4.869e+24, 6.0518e6) + EARTH = (3, 5.976e+24, 6.37814e6) + MARS = (6.421e+23, 3.3972e6) # Type checker error (optional) + JUPITER = 5 # Type checker error (optional) + + reveal_type(Planet.MERCURY.value) # Revealed type is Literal[1] (or int or object or Any) + + +The class ``enum.auto`` and method ``_generate_next_value_`` can be used within +an enum class to automatically generate values for enum members. Type checkers +may support these to infer literal types for member values:: + + class Color(Enum): + RED = auto() + GREEN = auto() + BLUE = auto() + + reveal_type(Color.RED.value) # Revealed type is Literal[1] (or int or object or Any) + + +If an enum class provides an explicit type annotation for ``_value_``, type +checkers should enforce this declared type when values are assigned to +``_value_``:: + + class Color(Enum): + _value_: int + RED = 1 # OK + GREEN = "green" # Type error + + class Planet(Enum): + _value_: str + + def __init__(self, value: int, mass: float, radius: float): + self._value_ = value # Type error + + MERCURY = (1, 3.303e+23, 2.4397e6) + +If the literal values for enum members are not supplied, as they sometimes +are not within a type stub file, a type checker can use the type of the +``_value_`` attribute:: + + class ColumnType(Enum): + _value_: int + DORIC = ... + IONIC = ... + CORINTHIAN = ... + + reveal_type(ColumnType.DORIC.value) # Revealed type is int (or object or Any) + + +Enum Literal Expansion +---------------------- + +From the perspective of the type system, most enum classes are equivalent +to the union of the literal members within that enum. (This rule +does not apply to classes that derive from ``enum.Flag`` because these enums +allow flags to be combined in arbitrary ways.) Because of the equivalency +between an enum class and the union of literal members within that enum, the +two types may be used interchangeably. Type checkers may therefore expand +an enum type (that does not derive from ``enum.Flag``) into a union of +literal values during type narrowing and exhaustion detection:: + + class Color(Enum): + RED = 1 + GREEN = 2 + BLUE = 3 + + def print_color1(c: Color): + if c is Color.RED or c is Color.BLUE: + print("red or blue") + else: + reveal_type(c) # Revealed type is Literal[Color.GREEN] + + def print_color2(c: Color): + match c: + case Color.RED | Color.BLUE: + print("red or blue") + case Color.GREEN: + print("green") + case _: + reveal_type(c) # Revealed type is Never + + +Likewise, a type checker should treat a complete union of all literal members +as compatible with the enum type:: + + class Answer(Enum): + Yes = 1 + No = 2 + + def func(val: object) -> Answer: + if val is not Answer.Yes and val is not Answer.No: + raise ValueError("Invalid value") + reveal_type(val) # Revealed type is Answer (or Literal[Answer.Yes, Answer.No]) + return val # OK diff --git a/docs/spec/index.rst b/docs/spec/index.rst index 6e5e5c00..30210227 100644 --- a/docs/spec/index.rst +++ b/docs/spec/index.rst @@ -24,6 +24,7 @@ Specification for the Python type system typeddict tuples namedtuples + enums narrowing directives distributing