Skip to content

Commit 459e610

Browse files
authored
Added draft chapter to typing spec for enumerations. (#1759)
This is the same set of changes as PR #1591. The CI script that enforces CLA signing was confused, so it didn't permit merging the original PR.
1 parent d442dcb commit 459e610

File tree

2 files changed

+364
-0
lines changed

2 files changed

+364
-0
lines changed

docs/spec/enums.rst

Lines changed: 363 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,363 @@
1+
Enumerations
2+
============
3+
4+
The ``enum.Enum`` class behaves differently from other Python classes in several
5+
ways that require special-case handling in type checkers. This section discusses
6+
the Enum behaviors that should be supported by type checkers and others which
7+
may be supported optionally. It is recommended that library and type stub
8+
authors avoid using optional behaviors because these may not be supported
9+
by some type checkers.
10+
11+
12+
Enum Definition
13+
---------------
14+
15+
Enum classes can be defined using a "class syntax" or a "function syntax".
16+
The function syntax offers several ways to specify enum members: names passed
17+
as individual arguments, a list or tuple of names, a string of
18+
comma-delimited or space-delimited names, a list or tuple of tuples that contain
19+
name/value pairs, and a dictionary of name/value items.
20+
21+
Type checkers should support the class syntax, but the function syntax (in
22+
its various forms) is optional::
23+
24+
class Color1(Enum): # Supported
25+
RED = 1
26+
GREEN = 2
27+
BLUE = 3
28+
29+
Color2 = Enum('Color2', 'RED', 'GREEN', 'BLUE') # Optional
30+
Color3 = Enum('Color3', ['RED', 'GREEN', 'BLUE']) # Optional
31+
Color4 = Enum('Color4', ('RED', 'GREEN', 'BLUE')) # Optional
32+
Color5 = Enum('Color5', 'RED, GREEN, BLUE') # Optional
33+
Color6 = Enum('Color6', 'RED GREEN BLUE') # Optional
34+
Color7 = Enum('Color7', [('RED': 1), ('GREEN': 2), ('BLUE': 3)]) # Optional
35+
Color8 = Enum('Color8', (('RED': 1), ('GREEN': 2), ('BLUE': 3))) # Optional
36+
Color9 = Enum('Color9', {'RED': 1, 'GREEN': 2, 'BLUE': 3}) # Optional
37+
38+
Enum classes can also be defined using a subclass of ``enum.Enum`` or any class
39+
that uses ``enum.EnumType`` (or a subclass thereof) as a metaclass. Note that
40+
``enum.EnumType`` was named ``enum.EnumMeta`` prior to Python 3.11. Type
41+
checkers should treat such classes as enums::
42+
43+
class CustomEnum1(Enum):
44+
pass
45+
46+
class Color7(CustomEnum1): # Supported
47+
RED = 1
48+
GREEN = 2
49+
BLUE = 3
50+
51+
class CustomEnumType(EnumType):
52+
pass
53+
54+
class CustomEnum2(metaclass=CustomEnumType):
55+
pass
56+
57+
class Color8(CustomEnum2): # Supported
58+
RED = 1
59+
GREEN = 2
60+
BLUE = 3
61+
62+
63+
Enum Behaviors
64+
--------------
65+
66+
Enum classes are iterable and indexable, and they can be called with a value
67+
to look up the enum member with that value. Type checkers should support these
68+
behaviors::
69+
70+
class Color(Enum):
71+
RED = 1
72+
GREEN = 2
73+
BLUE = 3
74+
75+
for color in Color:
76+
reveal_type(color) # Revealed type is 'Color'
77+
78+
reveal_type(Color["RED"]) # Revealed type is 'Literal[Color.RED]' (or 'Color')
79+
reveal_type(Color(3)) # Revealed type is 'Literal[Color.BLUE]' (or 'Color')
80+
81+
Unlike most Python classes, Calling an enum class does not invoke its constructor.
82+
Instead, the call performs a value-based lookup of an enum member.
83+
84+
An Enum class with one or more defined members cannot be subclassed. They are
85+
implicitly "final". Type checkers should enforce this::
86+
87+
class EnumWithNoMembers(Enum):
88+
pass
89+
90+
class Shape(EnumWithNoMembers): # OK (because no members are defined)
91+
SQUARE = 1
92+
CIRCLE = 2
93+
94+
class ExtendedShape(Shape): # Type checker error: Shape is implicitly final
95+
TRIANGLE = 3
96+
97+
98+
Defining Members
99+
----------------
100+
101+
When using the "class syntax", enum classes can define both members and
102+
other (non-member) attributes. The ``EnumType`` metaclass applies a set
103+
of rules to distinguish between members and non-members. Type checkers
104+
should honor the most common of these rules. The lesser-used rules are
105+
optional. Some of these rules may be impossible to evaluate and enforce
106+
statically in cases where dynamic values are used.
107+
108+
* If an attribute is defined in the class body with a type annotation but
109+
with no assigned value, a type checker should assume this is a non-member
110+
attribute::
111+
112+
class Pet(Enum):
113+
genus: str # Non-member attribute
114+
species: str # Non-member attribute
115+
116+
CAT = 1 # Member attribute
117+
DOG = 2 # Member attribute
118+
119+
Within a type stub, members can be defined using the actual runtime values,
120+
or a placeholder of ``...`` can be used::
121+
122+
class Pet(Enum):
123+
genus: str # Non-member attribute
124+
species: str # Non-member attribute
125+
126+
CAT = ... # Member attribute
127+
DOG = ... # Member attribute
128+
129+
* Members defined within an enum class should not include explicit type
130+
annotations. Type checkers should infer a literal type for all members.
131+
A type checker should report an error if a type annotation is used
132+
for an enum member because this type will be incorrect and misleading
133+
to readers of the code::
134+
135+
class Pet(Enum):
136+
CAT = 1 # OK
137+
DOG: int = 2 # Type checker error
138+
139+
* Methods, callables, descriptors (including properties), and nested classes
140+
that are defined in the class are not treated as enum members by the
141+
``EnumType`` metaclass and should likewise not be treated as enum members by
142+
a type checker::
143+
144+
def identity(x): return x
145+
146+
class Pet(Enum):
147+
CAT = 1 # Member attribute
148+
DOG = 2 # Member attribute
149+
150+
converter = lambda x: str(x) # Non-member attribute
151+
transform = identity # Non-member attribute
152+
153+
@property
154+
def species(self) -> str: # Non-member property
155+
return "mammal"
156+
157+
def speak(self) -> None: # Non-member method
158+
print("meow" if self is Pet.CAT else "woof")
159+
160+
class Nested: ... # Non-member nested class
161+
162+
* An attribute that is assigned the value of another member of the same enum
163+
is not a member itself. Instead, it is an alias for the first member::
164+
165+
class TrafficLight(Enum):
166+
RED = 1
167+
GREEN = 2
168+
YELLOW = 3
169+
170+
AMBER = YELLOW # Alias for YELLOW
171+
172+
reveal_type(TrafficLight.AMBER) # Revealed type is Literal[TrafficLight.YELLOW]
173+
174+
* If using Python 3.11 or newer, the ``enum.member`` and ``enum.nonmember``
175+
classes can be used to unambiguously distinguish members from non-members.
176+
Type checkers should support these classes::
177+
178+
class Example(Enum):
179+
a = member(1) # Member attribute
180+
b = nonmember(2) # Non-member attribute
181+
182+
@member
183+
def c(self) -> None: # Member method
184+
pass
185+
186+
reveal_type(Example.a) # Revealed type is Literal[Example.a]
187+
reveal_type(Example.b) # Revealed type is int or Literal[2]
188+
reveal_type(Example.c) # Revealed type is Literal[Example.c]
189+
190+
* An attribute with a private name (beginning with, but not ending in, a double
191+
underscore) is treated as a non-member.
192+
193+
class Example(Enum):
194+
A = 1 # Member attribute
195+
__B = 2 # Non-member attribute
196+
197+
reveal_type(Example.A) # Revealed type is Literal[Example.A]
198+
reveal_type(Example.__B) # Type Error: Private name is mangled
199+
200+
* An enum class can define a class symbol named ``_ignore_``. This can be a list
201+
of names or a string containing a space-delimited list of names that are
202+
deleted from the enum class at runtime. Type checkers may support this
203+
mechanism::
204+
205+
class Pet(Enum):
206+
_ignore_ = "DOG FISH"
207+
CAT = 1 # Member attribute
208+
DOG = 2 # temporary variable, will be removed from the final enum class
209+
FISH = 3 # temporary variable, will be removed from the final enum class
210+
211+
212+
Member Names
213+
------------
214+
215+
All enum member objects have an attribute ``_name_`` that contains the member's
216+
name. They also have a property ``name`` that returns the same name. Type
217+
checkers may infer a literal type for the name of a member::
218+
219+
class Color(Enum):
220+
RED = 1
221+
GREEN = 2
222+
BLUE = 3
223+
224+
reveal_type(Color.RED._name_) # Revealed type is Literal["RED"] (or str)
225+
reveal_type(Color.RED.name) # Revealed type is Literal["RED"] (or str)
226+
227+
def func1(red_or_blue: Literal[Color.RED, Color.BLUE]):
228+
reveal_type(red_or_blue.name) # Revealed type is Literal["RED", "BLUE"] (or str)
229+
230+
def func2(any_color: Color):
231+
reveal_type(any_color.name) # Revealed type is Literal["RED", "BLUE", "GREEN"] (or str)
232+
233+
234+
Member Values
235+
-------------
236+
237+
All enum member objects have an attribute ``_value_`` that contains the member's
238+
value. They also have a property ``value`` that returns the same value. Type
239+
checkers may infer the type of a member's value::
240+
241+
class Color(Enum):
242+
RED = 1
243+
GREEN = 2
244+
BLUE = 3
245+
246+
reveal_type(Color.RED._value_) # Revealed type is Literal[1] (or int or object or Any)
247+
reveal_type(Color.RED.value) # Revealed type is Literal[1] (or int or object or Any)
248+
249+
def func1(red_or_blue: Literal[Color.RED, Color.BLUE]):
250+
reveal_type(red_or_blue.value) # Revealed type is Literal[1, 2] (or int or object or Any)
251+
252+
def func2(any_color: Color):
253+
reveal_type(any_color.value) # Revealed type is Literal[1, 2, 3] (or int or object or Any)
254+
255+
256+
The value of ``_value_`` can be assigned in a constructor method. This technique
257+
is sometimes used to initialize both the member value and non-member attributes.
258+
If the value assigned in the class body is a tuple, the unpacked tuple value is
259+
passed to the constructor. Type checkers may validate consistency between assigned
260+
tuple values and the constructor signature::
261+
262+
class Planet(Enum):
263+
def __init__(self, value: int, mass: float, radius: float):
264+
self._value_ = value
265+
self.mass = mass
266+
self.radius = radius
267+
268+
MERCURY = (1, 3.303e+23, 2.4397e6)
269+
VENUS = (2, 4.869e+24, 6.0518e6)
270+
EARTH = (3, 5.976e+24, 6.37814e6)
271+
MARS = (6.421e+23, 3.3972e6) # Type checker error (optional)
272+
JUPITER = 5 # Type checker error (optional)
273+
274+
reveal_type(Planet.MERCURY.value) # Revealed type is Literal[1] (or int or object or Any)
275+
276+
277+
The class ``enum.auto`` and method ``_generate_next_value_`` can be used within
278+
an enum class to automatically generate values for enum members. Type checkers
279+
may support these to infer literal types for member values::
280+
281+
class Color(Enum):
282+
RED = auto()
283+
GREEN = auto()
284+
BLUE = auto()
285+
286+
reveal_type(Color.RED.value) # Revealed type is Literal[1] (or int or object or Any)
287+
288+
289+
If an enum class provides an explicit type annotation for ``_value_``, type
290+
checkers should enforce this declared type when values are assigned to
291+
``_value_``::
292+
293+
class Color(Enum):
294+
_value_: int
295+
RED = 1 # OK
296+
GREEN = "green" # Type error
297+
298+
class Planet(Enum):
299+
_value_: str
300+
301+
def __init__(self, value: int, mass: float, radius: float):
302+
self._value_ = value # Type error
303+
304+
MERCURY = (1, 3.303e+23, 2.4397e6)
305+
306+
If the literal values for enum members are not supplied, as they sometimes
307+
are not within a type stub file, a type checker can use the type of the
308+
``_value_`` attribute::
309+
310+
class ColumnType(Enum):
311+
_value_: int
312+
DORIC = ...
313+
IONIC = ...
314+
CORINTHIAN = ...
315+
316+
reveal_type(ColumnType.DORIC.value) # Revealed type is int (or object or Any)
317+
318+
319+
Enum Literal Expansion
320+
----------------------
321+
322+
From the perspective of the type system, most enum classes are equivalent
323+
to the union of the literal members within that enum. (This rule
324+
does not apply to classes that derive from ``enum.Flag`` because these enums
325+
allow flags to be combined in arbitrary ways.) Because of the equivalency
326+
between an enum class and the union of literal members within that enum, the
327+
two types may be used interchangeably. Type checkers may therefore expand
328+
an enum type (that does not derive from ``enum.Flag``) into a union of
329+
literal values during type narrowing and exhaustion detection::
330+
331+
class Color(Enum):
332+
RED = 1
333+
GREEN = 2
334+
BLUE = 3
335+
336+
def print_color1(c: Color):
337+
if c is Color.RED or c is Color.BLUE:
338+
print("red or blue")
339+
else:
340+
reveal_type(c) # Revealed type is Literal[Color.GREEN]
341+
342+
def print_color2(c: Color):
343+
match c:
344+
case Color.RED | Color.BLUE:
345+
print("red or blue")
346+
case Color.GREEN:
347+
print("green")
348+
case _:
349+
reveal_type(c) # Revealed type is Never
350+
351+
352+
Likewise, a type checker should treat a complete union of all literal members
353+
as compatible with the enum type::
354+
355+
class Answer(Enum):
356+
Yes = 1
357+
No = 2
358+
359+
def func(val: object) -> Answer:
360+
if val is not Answer.Yes and val is not Answer.No:
361+
raise ValueError("Invalid value")
362+
reveal_type(val) # Revealed type is Answer (or Literal[Answer.Yes, Answer.No])
363+
return val # OK

docs/spec/index.rst

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -24,6 +24,7 @@ Specification for the Python type system
2424
typeddict
2525
tuples
2626
namedtuples
27+
enums
2728
narrowing
2829
directives
2930
distributing

0 commit comments

Comments
 (0)