PEP 586: Rewrite sections regarding enum (#997)

This commit adjusts two sections of this PEP that are related to enums.

First, it removes the sections regarding the interaction between enums,
imports, and Any. I wasn't aware that the import behavior described in
that section was mypy-only and isn't codified in PEP 484. So, I decided
to just remove that section entirely -- it didn't feel there was much I
could salvage there.

Instead, I opted to adjust the "invalid parameters" section to explain
in a little more detail why `Literal[Any]` is not allowed.

Second, I split up the section about type narrowing into two.

The first new section is a reminder that PEP 484 requires type checkers
to support certain kinds of exhaustibility checks when working with enums.
To make this more clear, I adjusted the example to be more closer to what
is used in the spec and removed any mention of reachability -- it felt
like a distraction.

The second section focuses back on some neat tricks using Literals that
type checkers may optionally implement. I also tweaked some of the
examples here as suggested in https://github.com/python/peps/pull/993.
This commit is contained in:
Michael Lee 2019-04-17 20:19:09 -07:00 committed by Guido van Rossum
parent 0e90d579b3
commit 2cb7fa99ce
1 changed files with 34 additions and 46 deletions

View File

@ -249,13 +249,15 @@ The following parameters are intentionally disallowed by design:
only types, never over values. only types, never over values.
The following are provisionally disallowed for simplicity. We can consider The following are provisionally disallowed for simplicity. We can consider
allowing them on a case-by-case basis based on demand. allowing them in future extensions of this PEP.
- Floats: e.g. ``Literal[3.14]``. Note: if we do decide to allow - Floats: e.g. ``Literal[3.14]``. Representing Literals of infinity or NaN
floats, we should likely disallow literal infinity and literal NaN. in a clean way is tricky; real-world APIs are unlikely to vary their
behavior based on a float parameter.
- Any: e.g. ``Literal[Any]`` Note: the semantics of what exactly
``Literal[Any]`` means would need to be clarified first. - Any: e.g. ``Literal[Any]``. ``Any`` is a type, and ``Literal[...]`` is
meant to contain values only. It is also unclear what ``Literal[Any]``
would actually semantically mean.
Parameters at runtime Parameters at runtime
--------------------- ---------------------
@ -293,26 +295,6 @@ In cases like these, we always assume the user meant to construct a
literal string. If the user wants a forward reference, they must wrap literal string. If the user wants a forward reference, they must wrap
the entire literal type in a string -- e.g. ``"Literal[Color.RED]"``. the entire literal type in a string -- e.g. ``"Literal[Color.RED]"``.
Literals, enums, and Any
------------------------
Another ambiguity is when the user attempts to use some expression that
is meant to be an enum but is actually of type ``Any``. For example,
suppose a user attempts to import an enum from a package with no type hints::
from typing import Literal
from lib_with_no_types import SomeEnum # SomeEnum has type 'Any'!
# x has type `Literal[Any]` due to the bad import
x: Literal[SomeEnum.FOO]
Because ``Literal`` may not be parameterized by ``Any``, this program
is *illegal*: the type checker should report an error with the last line.
In short, while ``Any`` may effectively be used as a placeholder for any
arbitrary *type*, it is currently **not** allowed to serve as a placeholder
for any arbitrary *value*.
Type inference Type inference
============== ==============
@ -517,20 +499,21 @@ We considered several different proposals for fixing this, but ultimately
decided to defer the problem of integer generics to a later date. See decided to defer the problem of integer generics to a later date. See
`Rejected or out-of-scope ideas`_ for more details. `Rejected or out-of-scope ideas`_ for more details.
Interactions with type narrowing Interactions with enums and exhaustiveness checks
-------------------------------- -------------------------------------------------
Type checkers should be capable of performing exhaustiveness checks when Type checkers should be capable of performing exhaustiveness checks when
working Literal types that have a closed number of variants, such as working Literal types that have a closed number of variants, such as
enums. For example, the type checker should be capable of inferring that the enums. For example, the type checker should be capable of inferring that
final ``else`` statement in the following function is unreachable:: the final ``else`` statement must be of type ``str``, since all three
values of the ``Status`` enum have already been exhausted::
class Status(Enum): class Status(Enum):
SUCCESS = 0 SUCCESS = 0
INVALID_DATA = 1 INVALID_DATA = 1
FATAL_ERROR = 2 FATAL_ERROR = 2
def parse_status(s: Status) -> None: def parse_status(s: Union[str, Status]) -> None:
if s is Status.SUCCESS: if s is Status.SUCCESS:
print("Success!") print("Success!")
elif s is Status.INVALID_DATA: elif s is Status.INVALID_DATA:
@ -538,24 +521,29 @@ final ``else`` statement in the following function is unreachable::
elif s is Status.FATAL_ERROR: elif s is Status.FATAL_ERROR:
print("Unexpected fatal error...") print("Unexpected fatal error...")
else: else:
# Error should not be reported by type checkers that # 's' must be of type 'str' since all other options are exhausted
# ignore errors in unreachable blocks print("Got custom status: " + s)
print("Nonsense" + 100)
This behavior is technically not new: this behavior is The interaction described above is not new: it's already
`already codified within PEP 484 <pep-484-enums_>`_. However, many type `already codified within PEP 484 <pep-484-enums_>`_. However, many type
checkers (such as mypy) do not yet implement this behavior. Once Literal checkers (such as mypy) do not yet implement this due to the expected
types are introduced, it will become easier to do so: we can model complexity of the implementation work.
enums as being approximately equal to the union of their values and
take advantage of any existing logic regarding unions, exhaustibility,
and type narrowing.
So here, ``Status`` could be treated as being approximately equal to Some of this complexity will be alleviated once Literal types are introduced:
``Literal[Status.SUCCESS, Status.INVALID_DATA, Status.FATAL_ERROR]`` rather than entirely special-casing enums, we can instead treat them as being
approximately equivalent to the union of their values and take advantage of any
existing logic regarding unions, exhaustibility, type narrowing, reachability,
and so forth the type checker might have already implemented.
So here, the ``Status`` enum could be treated as being approximately equivalent
to ``Literal[Status.SUCCESS, Status.INVALID_DATA, Status.FATAL_ERROR]``
and the type of ``s`` narrowed accordingly. and the type of ``s`` narrowed accordingly.
Type checkers may optionally perform additional analysis and narrowing Interactions with narrowing
beyond what is described above. ---------------------------
Type checkers may optionally perform additional analysis for both enum and
non-enum Literal types beyond what is described in the section above.
For example, it may be useful to perform narrowing based on things like For example, it may be useful to perform narrowing based on things like
containment or equality checks:: containment or equality checks::
@ -566,7 +554,7 @@ containment or equality checks::
# Literal["MALFORMED", "ABORTED"] here. # Literal["MALFORMED", "ABORTED"] here.
return expects_bad_status(status) return expects_bad_status(status)
# Similarly, type checker could narrow 'x' to Literal["PENDING"] # Similarly, type checker could narrow 'status' to Literal["PENDING"]
if status == "PENDING": if status == "PENDING":
expects_pending_status(status) expects_pending_status(status)
@ -577,7 +565,7 @@ involving Literal bools. For example, we can combine ``Literal[True]``,
@overload @overload
def is_int_like(x: Union[int, List[int]]) -> Literal[True]: ... def is_int_like(x: Union[int, List[int]]) -> Literal[True]: ...
@overload @overload
def is_int_like(x: Union[str, List[str]]) -> Literal[False]: ... def is_int_like(x: object) -> bool: ...
def is_int_like(x): ... def is_int_like(x): ...
vector: List[int] = [1, 2, 3] vector: List[int] = [1, 2, 3]