-
Notifications
You must be signed in to change notification settings - Fork 250
Commit
This commit does not belong to any branch on this repository, and may belong to a fork outside of the repository.
Added draft chapter to typing spec for enumerations.
- Loading branch information
Showing
2 changed files
with
309 additions
and
0 deletions.
There are no files selected for viewing
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -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 |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters