From c7ea13e84748801bf2b070de4aa40f651cee34fd Mon Sep 17 00:00:00 2001 From: Eric Traut Date: Wed, 17 Jan 2024 22:41:45 -0800 Subject: [PATCH 01/23] Added draft chapter to typing spec for enumerations. --- docs/spec/enums.rst | 308 ++++++++++++++++++++++++++++++++++++++++++++ docs/spec/index.rst | 1 + 2 files changed, 309 insertions(+) create mode 100644 docs/spec/enums.rst diff --git a/docs/spec/enums.rst b/docs/spec/enums.rst new file mode 100644 index 000000000..901da7fe9 --- /dev/null +++ b/docs/spec/enums.rst @@ -0,0 +1,308 @@ +Enumerations +============ + +Introduction +------------ + +The ``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 of names, a tuple of names, or a string of +comma-delimited names, or a string of space-delimited names. + +Type checkers should support the class syntax, but the function syntax (in +is 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 + +If a type checker supports the functional syntax, it should enforce name +consistency. That is, if the type is assigned to a variable, the name of +the variable must match the name of the enum class:: + + WrongName = Enum('Color', 'RED GREEN BLUE') # Type checker error + + +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') + +Calling an enum class does not invoke its constructor, as is normal +in most Python classes. The defined ``__new__`` method of an enum class is +replaced with a custom implementation that performs a value-based lookup of +an enum member. + +Enum members are immutable. Type checkers should generate an error if an enum +member is modified or deleted because these attempts will result in runtime +errors:: + + Color.RED = 5 # Type checker error + del Color.RED # Type checker error + +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 + + +Members and Non-Member Attributes +--------------------------------- + +When using the "class syntax", enum classes can define both members and +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 + + reveal_type(Pet.CAT) # Revealed type is Literal[Pet.CAT] + reveal_type(Pet.DOG) # Revealed type is Literal[Pet.DOG] + +* Methods, callables, and descriptors (including properties) that are defined +in the class are 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 + identity = 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") + + +* 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 + reveal_type(Example.c) # Revealed type is Literal[Example.c] + + +* 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 +excluded from the list of members at runtime. Type checkers may support this +mechanism. + + class Pet(Enum): + CAT = 1 # Member attribute + DOG = 2 # Non-member attribute + FISH = 3 # Non-member attribute + _ignore_ = "DOG FISH" + + reveal_type(Pet.CAT) # Revealed type is Literal[Pet.CAT] + reveal_type(Pet.DOG) # Revealed type is int (if _ignore_ is supported) + reveal_type(Pet.FISH) # Revealed type is int (if _ignore_ is supported) + + +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) + + +Enum Literal Expansion +---------------------- + +From the perspective of the type system, an enum class is equivalent to the union +of the literal members within that enum. Because of this equivalency, the +two types may be used interchangeably. Type checkers may therefore expand +an enum type 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 diff --git a/docs/spec/index.rst b/docs/spec/index.rst index dbee50c10..b5f9cbc71 100644 --- a/docs/spec/index.rst +++ b/docs/spec/index.rst @@ -20,6 +20,7 @@ Specification for the Python type system overload dataclasses typeddict + enums narrowing directives distributing From 4743b342818eb5b5666196a3e4a2067a0587de03 Mon Sep 17 00:00:00 2001 From: Eric Traut Date: Thu, 18 Jan 2024 00:07:55 -0800 Subject: [PATCH 02/23] Incorporated initial PR feedback. --- docs/spec/enums.rst | 29 ++++++++++++++++++++--------- 1 file changed, 20 insertions(+), 9 deletions(-) diff --git a/docs/spec/enums.rst b/docs/spec/enums.rst index 901da7fe9..39f8a861b 100644 --- a/docs/spec/enums.rst +++ b/docs/spec/enums.rst @@ -1,9 +1,6 @@ Enumerations ============ -Introduction ------------- - The ``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 @@ -64,8 +61,8 @@ in most Python classes. The defined ``__new__`` method of an enum class is replaced with a custom implementation that performs a value-based lookup of an enum member. -Enum members are immutable. Type checkers should generate an error if an enum -member is modified or deleted because these attempts will result in runtime +Enum classes are immutable. Type checkers should generate an error if an enum +is modified or deleted because these attempts will result in runtime errors:: Color.RED = 5 # Type checker error @@ -130,7 +127,7 @@ to readers of the code:: reveal_type(Pet.DOG) # Revealed type is Literal[Pet.DOG] * Methods, callables, and descriptors (including properties) that are defined -in the class are are not treated as enum members by the ``EnumType`` metaclass +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 @@ -140,7 +137,7 @@ and should likewise not be treated as enum members by a type checker:: DOG = 2 # Member attribute converter = lambda __x: str(__x) # Non-member attribute - identity = identity # Non-member attribute + transform = identity # Non-member attribute @property def species(self) -> str: # Non-member property @@ -231,7 +228,7 @@ 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. +tuple values and the constructor signature:: class Planet(Enum): def __init__(self, value: int, mass: float, radius: float): @@ -262,7 +259,7 @@ may support these to infer literal types for member values:: If an enum class provides an explicit type annotation for ``_value_``, type checkers should enforce this declared type when values are assigned to -``_value__`:: +``_value_``:: class Color(Enum): _value_: int @@ -306,3 +303,17 @@ exhaustion detection:: 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 From 304936af82ed79e1afc3c25bac2be2530d252da3 Mon Sep 17 00:00:00 2001 From: Sebastian Rittau Date: Thu, 18 Jan 2024 12:15:40 +0100 Subject: [PATCH 03/23] Fix formatting of list items --- docs/spec/enums.rst | 30 +++++++++++++++--------------- 1 file changed, 15 insertions(+), 15 deletions(-) diff --git a/docs/spec/enums.rst b/docs/spec/enums.rst index 39f8a861b..0b6c7cbe6 100644 --- a/docs/spec/enums.rst +++ b/docs/spec/enums.rst @@ -93,8 +93,8 @@ 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:: + with no assigned value, a type checker should assume this is a non-member + attribute:: class Pet(Enum): genus: str # Non-member attribute @@ -103,8 +103,8 @@ 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:: + 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 @@ -114,10 +114,10 @@ or a placeholder of ``...`` can be used:: 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:: + 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 @@ -127,8 +127,8 @@ to readers of the code:: reveal_type(Pet.DOG) # Revealed type is Literal[Pet.DOG] * Methods, callables, and descriptors (including properties) 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:: + 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 @@ -148,8 +148,8 @@ and should likewise not be treated as enum members by a type checker:: * 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. + 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 @@ -165,9 +165,9 @@ Type checkers should support these classes. * 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 -excluded from the list of members at runtime. Type checkers may support this -mechanism. + of names or a string containing a space-delimited list of names that are + excluded from the list of members at runtime. Type checkers may support this + mechanism. class Pet(Enum): CAT = 1 # Member attribute From 0f7eb3cceb4edfe0d9717351299d07db7ca3a9e0 Mon Sep 17 00:00:00 2001 From: Sebastian Rittau Date: Thu, 18 Jan 2024 12:19:03 +0100 Subject: [PATCH 04/23] Fix code formatting --- docs/spec/enums.rst | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/docs/spec/enums.rst b/docs/spec/enums.rst index 0b6c7cbe6..783fa86bb 100644 --- a/docs/spec/enums.rst +++ b/docs/spec/enums.rst @@ -149,7 +149,7 @@ where dynamic values are used. * 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. + Type checkers should support these classes:: class Example(Enum): a = member(1) # Member attribute @@ -167,7 +167,7 @@ where dynamic values are used. * 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 excluded from the list of members at runtime. Type checkers may support this - mechanism. + mechanism:: class Pet(Enum): CAT = 1 # Member attribute From 24b45ca458f88d4b6b8870d44b47db114e9d7b08 Mon Sep 17 00:00:00 2001 From: Eric Traut Date: Thu, 18 Jan 2024 08:53:16 -0800 Subject: [PATCH 05/23] Incorporated additional PR feedback. --- docs/spec/enums.rst | 35 +++++++++++++++++++++++++++++------ 1 file changed, 29 insertions(+), 6 deletions(-) diff --git a/docs/spec/enums.rst b/docs/spec/enums.rst index 783fa86bb..9b10af23c 100644 --- a/docs/spec/enums.rst +++ b/docs/spec/enums.rst @@ -1,7 +1,7 @@ Enumerations ============ -The ``Enum`` class behaves differently from other Python classes in several +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 @@ -25,11 +25,11 @@ is various forms) is optional:: 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 + 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 If a type checker supports the functional syntax, it should enforce name consistency. That is, if the type is assigned to a variable, the name of @@ -37,6 +37,29 @@ the variable must match the name of the enum class:: WrongName = Enum('Color', 'RED GREEN BLUE') # Type checker error +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. 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 -------------- From 4f287221407984f1f11b8a374e8861de0ba901e6 Mon Sep 17 00:00:00 2001 From: Eric Traut Date: Thu, 18 Jan 2024 08:53:56 -0800 Subject: [PATCH 06/23] Fixed a few more formatting issues. --- docs/spec/enums.rst | 10 +++++----- 1 file changed, 5 insertions(+), 5 deletions(-) diff --git a/docs/spec/enums.rst b/docs/spec/enums.rst index 9b10af23c..23a4e908b 100644 --- a/docs/spec/enums.rst +++ b/docs/spec/enums.rst @@ -35,7 +35,7 @@ If a type checker supports the functional syntax, it should enforce name consistency. That is, if the type is assigned to a variable, the name of the variable must match the name of the enum class:: - WrongName = Enum('Color', 'RED GREEN BLUE') # Type checker error + WrongName = Enum('Color', 'RED GREEN BLUE') # Type checker error 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. Type @@ -74,10 +74,10 @@ behaviors:: BLUE = 3 for color in Color: - reveal_type(color) # Revealed type is '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') + reveal_type(Color["RED"]) # Revealed type is 'Literal[Color.RED]' (or 'Color') + reveal_type(Color(3)) # Revealed type is 'Literal[Color.BLUE]' (or 'Color') Calling an enum class does not invoke its constructor, as is normal in most Python classes. The defined ``__new__`` method of an enum class is @@ -277,7 +277,7 @@ may support these to infer literal types for member values:: GREEN = auto() BLUE = auto() - 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) If an enum class provides an explicit type annotation for ``_value_``, type From 81dfbfdf63f725b6156ffc08b06bac8d22e06bb3 Mon Sep 17 00:00:00 2001 From: Eric Traut Date: Thu, 18 Jan 2024 11:57:19 -0800 Subject: [PATCH 07/23] Incorporated additional PR feedback. --- docs/spec/enums.rst | 38 +++++++++++++++++--------------------- 1 file changed, 17 insertions(+), 21 deletions(-) diff --git a/docs/spec/enums.rst b/docs/spec/enums.rst index 23a4e908b..b8021e303 100644 --- a/docs/spec/enums.rst +++ b/docs/spec/enums.rst @@ -14,8 +14,9 @@ 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 of names, a tuple of names, or a string of -comma-delimited names, or a string of space-delimited names. +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 is various forms) is optional:: @@ -30,6 +31,9 @@ is various forms) is 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 If a type checker supports the functional syntax, it should enforce name consistency. That is, if the type is assigned to a variable, the name of @@ -38,8 +42,8 @@ the variable must match the name of the enum class:: WrongName = Enum('Color', 'RED GREEN BLUE') # Type checker error 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. Type -checkers should treat such classes as enums:: +that uses ``enum.EnumType`` or ``enum.EnumMeta`` (or a subclass thereof) as a +metaclass. Type checkers should treat such classes as enums:: class CustomEnum1(Enum): pass @@ -84,9 +88,8 @@ in most Python classes. The defined ``__new__`` method of an enum class is replaced with a custom implementation that performs a value-based lookup of an enum member. -Enum classes are immutable. Type checkers should generate an error if an enum -is modified or deleted because these attempts will result in runtime -errors:: +Type checkers should generate an error if an enum member is overwritten or +deleted because these will result in runtime errors:: Color.RED = 5 # Type checker error del Color.RED # Type checker error @@ -109,11 +112,11 @@ Members and Non-Member Attributes --------------------------------- When using the "class syntax", enum classes can define both members and -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. +non-member attributes. The ``EnumType`` metaclass (named ``EnumMeta`` prior to +Python 3.11) 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 @@ -146,9 +149,6 @@ where dynamic values are used. CAT = 1 # OK DOG: int = 2 # Type checker error - reveal_type(Pet.CAT) # Revealed type is Literal[Pet.CAT] - reveal_type(Pet.DOG) # Revealed type is Literal[Pet.DOG] - * Methods, callables, and descriptors (including properties) 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:: @@ -189,18 +189,14 @@ where dynamic values are used. * 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 - excluded from the list of members at runtime. Type checkers may support this + 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 # Non-member attribute FISH = 3 # Non-member attribute - _ignore_ = "DOG FISH" - - reveal_type(Pet.CAT) # Revealed type is Literal[Pet.CAT] - reveal_type(Pet.DOG) # Revealed type is int (if _ignore_ is supported) - reveal_type(Pet.FISH) # Revealed type is int (if _ignore_ is supported) Member Names From 07efd4f0206cf4964f79bcf6025305b6be44f5b6 Mon Sep 17 00:00:00 2001 From: Eric Traut Date: Thu, 18 Jan 2024 12:07:50 -0800 Subject: [PATCH 08/23] Update docs/spec/enums.rst Co-authored-by: Ethan Furman --- docs/spec/enums.rst | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/docs/spec/enums.rst b/docs/spec/enums.rst index b8021e303..4b958d588 100644 --- a/docs/spec/enums.rst +++ b/docs/spec/enums.rst @@ -195,8 +195,8 @@ evaluate and enforce statically in cases where dynamic values are used. class Pet(Enum): _ignore_ = "DOG FISH" CAT = 1 # Member attribute - DOG = 2 # Non-member attribute - FISH = 3 # Non-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 From 37714b9c57189de750e651f497c97a303dd108db Mon Sep 17 00:00:00 2001 From: Eric Traut Date: Thu, 18 Jan 2024 16:52:50 -0800 Subject: [PATCH 09/23] Tried to improve language around "members" to reduce confusion. --- docs/spec/enums.rst | 12 ++++++------ 1 file changed, 6 insertions(+), 6 deletions(-) diff --git a/docs/spec/enums.rst b/docs/spec/enums.rst index 4b958d588..d16fe94f3 100644 --- a/docs/spec/enums.rst +++ b/docs/spec/enums.rst @@ -108,14 +108,14 @@ implicitly "final". Type checkers should enforce this:: TRIANGLE = 3 -Members and Non-Member Attributes ---------------------------------- +Defining Members +---------------- When using the "class syntax", enum classes can define both members and -non-member attributes. The ``EnumType`` metaclass (named ``EnumMeta`` prior to -Python 3.11) 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 +other (non-member) attributes. The ``EnumType`` metaclass (named ``EnumMeta`` +prior to Python 3.11) 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 From c7b089720f06a45d0d2de0022488766e4de4c49a Mon Sep 17 00:00:00 2001 From: Eric Traut Date: Thu, 18 Jan 2024 18:36:37 -0800 Subject: [PATCH 10/23] Additional PR feedback. --- docs/spec/enums.rst | 6 ------ 1 file changed, 6 deletions(-) diff --git a/docs/spec/enums.rst b/docs/spec/enums.rst index d16fe94f3..53f1c47c8 100644 --- a/docs/spec/enums.rst +++ b/docs/spec/enums.rst @@ -88,12 +88,6 @@ in most Python classes. The defined ``__new__`` method of an enum class is replaced with a custom implementation that performs a value-based lookup of an enum member. -Type checkers should generate an error if an enum member is overwritten or -deleted because these will result in runtime errors:: - - Color.RED = 5 # Type checker error - del Color.RED # Type checker error - An Enum class with one or more defined members cannot be subclassed. They are implicitly "final". Type checkers should enforce this:: From 57567b10e2bb5d5100004a8680d8aca0b1ae44ce Mon Sep 17 00:00:00 2001 From: Eric Traut Date: Sun, 21 Jan 2024 17:11:21 -0800 Subject: [PATCH 11/23] Incorporated feedback from Jelle. --- docs/spec/enums.rst | 17 +++++++++-------- 1 file changed, 9 insertions(+), 8 deletions(-) diff --git a/docs/spec/enums.rst b/docs/spec/enums.rst index 53f1c47c8..29ef66796 100644 --- a/docs/spec/enums.rst +++ b/docs/spec/enums.rst @@ -19,7 +19,7 @@ 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 -is various forms) is optional:: +its various forms) is optional:: class Color1(Enum): # Supported RED = 1 @@ -42,8 +42,9 @@ the variable must match the name of the enum class:: WrongName = Enum('Color', 'RED GREEN BLUE') # Type checker error Enum classes can also be defined using a subclass of ``enum.Enum`` or any class -that uses ``enum.EnumType`` or ``enum.EnumMeta`` (or a subclass thereof) as a -metaclass. Type checkers should treat such classes as enums:: +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 @@ -106,11 +107,11 @@ Defining Members ---------------- When using the "class syntax", enum classes can define both members and -other (non-member) attributes. The ``EnumType`` metaclass (named ``EnumMeta`` -prior to Python 3.11) 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. +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 From b2cb59691eae82302362db8f6874e57097ce125a Mon Sep 17 00:00:00 2001 From: Eric Traut Date: Tue, 23 Jan 2024 19:22:59 -0800 Subject: [PATCH 12/23] Incorporated PR feedback from @rchen152. --- docs/spec/enums.rst | 6 ++---- 1 file changed, 2 insertions(+), 4 deletions(-) diff --git a/docs/spec/enums.rst b/docs/spec/enums.rst index 29ef66796..a9c424eed 100644 --- a/docs/spec/enums.rst +++ b/docs/spec/enums.rst @@ -84,10 +84,8 @@ behaviors:: reveal_type(Color["RED"]) # Revealed type is 'Literal[Color.RED]' (or 'Color') reveal_type(Color(3)) # Revealed type is 'Literal[Color.BLUE]' (or 'Color') -Calling an enum class does not invoke its constructor, as is normal -in most Python classes. The defined ``__new__`` method of an enum class is -replaced with a custom implementation that performs a value-based lookup of -an enum member. +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:: From 19886266f5323f1b9fc3d7c0345c053ae854935b Mon Sep 17 00:00:00 2001 From: Eric Traut Date: Tue, 23 Jan 2024 19:28:47 -0800 Subject: [PATCH 13/23] Removed the requirement that type checkers enforce name consistency for enums declared with functional syntax. --- docs/spec/enums.rst | 6 ------ 1 file changed, 6 deletions(-) diff --git a/docs/spec/enums.rst b/docs/spec/enums.rst index a9c424eed..fee13dbae 100644 --- a/docs/spec/enums.rst +++ b/docs/spec/enums.rst @@ -35,12 +35,6 @@ its various forms) is optional:: Color8 = Enum('Color8', (('RED': 1), ('GREEN': 2), ('BLUE': 3))) # Optional Color9 = Enum('Color9', {'RED': 1, 'GREEN': 2, 'BLUE': 3}) # Optional -If a type checker supports the functional syntax, it should enforce name -consistency. That is, if the type is assigned to a variable, the name of -the variable must match the name of the enum class:: - - WrongName = Enum('Color', 'RED GREEN BLUE') # Type checker error - 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 From 20ed1f2fdd416306708a6d7b51ecb0ff90ee1d00 Mon Sep 17 00:00:00 2001 From: Eric Traut Date: Tue, 23 Jan 2024 22:21:50 -0800 Subject: [PATCH 14/23] Added an example based on @rchen152's PR feedback. --- docs/spec/enums.rst | 12 ++++++++++++ 1 file changed, 12 insertions(+) diff --git a/docs/spec/enums.rst b/docs/spec/enums.rst index fee13dbae..63feade6c 100644 --- a/docs/spec/enums.rst +++ b/docs/spec/enums.rst @@ -280,6 +280,18 @@ checkers should enforce this declared type when values are assigned to 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 cc69bbf16b1d158e7415a7f8a494b3ba3e218181 Mon Sep 17 00:00:00 2001 From: Eric Traut Date: Sat, 27 Jan 2024 14:11:34 -0800 Subject: [PATCH 15/23] Added another special case that EnumMeta uses for determining whether an attribute is a member. --- docs/spec/enums.rst | 12 +++++++++++- 1 file changed, 11 insertions(+), 1 deletion(-) diff --git a/docs/spec/enums.rst b/docs/spec/enums.rst index 63feade6c..68ac10194 100644 --- a/docs/spec/enums.rst +++ b/docs/spec/enums.rst @@ -156,6 +156,17 @@ statically in cases where dynamic values are used. def speak(self) -> None: # Non-member method print("meow" if self is Pet.CAT else "woof") +* 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. @@ -173,7 +184,6 @@ statically in cases where dynamic values are used. reveal_type(Example.b) # Revealed type is int reveal_type(Example.c) # Revealed type is Literal[Example.c] - * 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 From f6b4ae57cee563d30368ef89fd3ee34a9ce1d25f Mon Sep 17 00:00:00 2001 From: Eric Traut Date: Wed, 27 Mar 2024 17:46:22 -0600 Subject: [PATCH 16/23] Added an exception to the rule about Enum literal expansion. It doesn't apply to enum classes that derive from `enum.Flags` because these classes support enum members with values of zero and all combinations of the defined flags. --- docs/spec/enums.rst | 11 +++++++---- 1 file changed, 7 insertions(+), 4 deletions(-) diff --git a/docs/spec/enums.rst b/docs/spec/enums.rst index 68ac10194..df86fe127 100644 --- a/docs/spec/enums.rst +++ b/docs/spec/enums.rst @@ -306,11 +306,14 @@ are not within a type stub file, a type checker can use the type of the Enum Literal Expansion ---------------------- -From the perspective of the type system, an enum class is equivalent to the union -of the literal members within that enum. Because of this equivalency, the +From the perspective of the type system, an 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.Flags`` 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 into a union of literal values during type narrowing and -exhaustion detection:: +an enum type (that does not derive from ``enum.Flags``) into a union of +literal values during type narrowing and exhaustion detection:: class Color(Enum): RED = 1 From 79e64e8ee81b36cb7a4ea05963c0901be5767e7c Mon Sep 17 00:00:00 2001 From: Eric Traut Date: Wed, 27 Mar 2024 17:52:01 -0600 Subject: [PATCH 17/23] =?UTF-8?q?Fixed=20type=20=E2=80=94=C2=A0should=20be?= =?UTF-8?q?=20`enum.Flag`,=20not=20`enum.Flags`.?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- docs/spec/enums.rst | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/docs/spec/enums.rst b/docs/spec/enums.rst index df86fe127..57ea9b689 100644 --- a/docs/spec/enums.rst +++ b/docs/spec/enums.rst @@ -308,11 +308,11 @@ Enum Literal Expansion From the perspective of the type system, an 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.Flags`` because these enums +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.Flags``) into a union of +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): From 6332c4e1abf0b119cbc81d9a1b99a077681e7603 Mon Sep 17 00:00:00 2001 From: Eric Traut Date: Mon, 1 Apr 2024 07:06:20 -0700 Subject: [PATCH 18/23] Update docs/spec/enums.rst Co-authored-by: Hashem --- docs/spec/enums.rst | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/docs/spec/enums.rst b/docs/spec/enums.rst index 57ea9b689..9eefc95f9 100644 --- a/docs/spec/enums.rst +++ b/docs/spec/enums.rst @@ -306,7 +306,7 @@ are not within a type stub file, a type checker can use the type of the Enum Literal Expansion ---------------------- -From the perspective of the type system, an most enum classes are equivalent +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 From f55f9c0ce30c7d2294d9b3f9f0f0745a786c2caf Mon Sep 17 00:00:00 2001 From: Eric Traut Date: Thu, 4 Apr 2024 16:48:18 -0700 Subject: [PATCH 19/23] Updated enum spec to include another special case for determining whether an attribute is an enum member. Attributes with private (mangled) names are exempt. --- docs/spec/enums.rst | 10 ++++++++++ 1 file changed, 10 insertions(+) diff --git a/docs/spec/enums.rst b/docs/spec/enums.rst index 9eefc95f9..648fb7015 100644 --- a/docs/spec/enums.rst +++ b/docs/spec/enums.rst @@ -184,6 +184,16 @@ statically in cases where dynamic values are used. reveal_type(Example.b) # Revealed type is int 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 From 0a719a6365278e2fe963bc6f14a9edce88e45e61 Mon Sep 17 00:00:00 2001 From: Eric Traut Date: Mon, 15 Apr 2024 19:20:38 -0700 Subject: [PATCH 20/23] Added mention of nested classes being treated as non-members for enums. --- docs/spec/enums.rst | 9 ++++++--- 1 file changed, 6 insertions(+), 3 deletions(-) diff --git a/docs/spec/enums.rst b/docs/spec/enums.rst index 648fb7015..d309b8642 100644 --- a/docs/spec/enums.rst +++ b/docs/spec/enums.rst @@ -136,9 +136,10 @@ statically in cases where dynamic values are used. CAT = 1 # OK DOG: int = 2 # Type checker error -* Methods, callables, and descriptors (including properties) 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:: +* Methods, callables, and 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 @@ -155,6 +156,8 @@ statically in cases where dynamic values are used. 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:: From 6479101e414f16d546a34d7ba2fe31bac937ee44 Mon Sep 17 00:00:00 2001 From: "pre-commit-ci[bot]" <66853113+pre-commit-ci[bot]@users.noreply.github.com> Date: Tue, 16 Apr 2024 02:20:49 +0000 Subject: [PATCH 21/23] [pre-commit.ci] auto fixes from pre-commit.com hooks --- docs/spec/enums.rst | 16 ++++++++-------- 1 file changed, 8 insertions(+), 8 deletions(-) diff --git a/docs/spec/enums.rst b/docs/spec/enums.rst index d309b8642..a46065845 100644 --- a/docs/spec/enums.rst +++ b/docs/spec/enums.rst @@ -1,7 +1,7 @@ Enumerations ============ -The ``enum.Enum`` class behaves differently from other Python classes in several +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 @@ -42,7 +42,7 @@ checkers should treat such classes as enums:: class CustomEnum1(Enum): pass - + class Color7(CustomEnum1): # Supported RED = 1 GREEN = 2 @@ -50,7 +50,7 @@ checkers should treat such classes as enums:: class CustomEnumType(EnumType): pass - + class CustomEnum2(metaclass=CustomEnumType): pass @@ -146,17 +146,17 @@ statically in cases where dynamic values are used. 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 @@ -312,7 +312,7 @@ are not within a type stub file, a type checker can use the type of the DORIC = ... IONIC = ... CORINTHIAN = ... - + reveal_type(ColumnType.DORIC.value) # Revealed type is int (or object or Any) @@ -332,7 +332,7 @@ literal values during type narrowing and exhaustion detection:: 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") From 997902273a95954d2f528609fda1c4d0dd5a577d Mon Sep 17 00:00:00 2001 From: Eric Traut Date: Thu, 23 May 2024 12:22:14 -0700 Subject: [PATCH 22/23] Incorporated PR feedback from @stroxler. --- docs/spec/enums.rst | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/docs/spec/enums.rst b/docs/spec/enums.rst index a46065845..6efae41b3 100644 --- a/docs/spec/enums.rst +++ b/docs/spec/enums.rst @@ -136,18 +136,18 @@ statically in cases where dynamic values are used. CAT = 1 # OK DOG: int = 2 # Type checker error -* Methods, callables, and descriptors (including properties), and nested classes +* 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 + def identity(x): return x class Pet(Enum): CAT = 1 # Member attribute DOG = 2 # Member attribute - converter = lambda __x: str(__x) # Non-member attribute + converter = lambda x: str(x) # Non-member attribute transform = identity # Non-member attribute @property From bcc2469df889e27cd3734f1bf5a9e36ebeda6958 Mon Sep 17 00:00:00 2001 From: Eric Traut Date: Sun, 2 Jun 2024 11:22:08 -0700 Subject: [PATCH 23/23] Incorporated review feedback from @carljm. --- docs/spec/enums.rst | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/docs/spec/enums.rst b/docs/spec/enums.rst index 6efae41b3..b9759e545 100644 --- a/docs/spec/enums.rst +++ b/docs/spec/enums.rst @@ -184,7 +184,7 @@ statically in cases where dynamic values are used. pass reveal_type(Example.a) # Revealed type is Literal[Example.a] - reveal_type(Example.b) # Revealed type is int + 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