From 2cb7fa99cea545849cb459ce59d8c791c927c525 Mon Sep 17 00:00:00 2001 From: Michael Lee Date: Wed, 17 Apr 2019 20:19:09 -0700 Subject: [PATCH] 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. --- pep-0586.rst | 80 ++++++++++++++++++++++------------------------------ 1 file changed, 34 insertions(+), 46 deletions(-) diff --git a/pep-0586.rst b/pep-0586.rst index 4451fe3d8..0bdf5133a 100644 --- a/pep-0586.rst +++ b/pep-0586.rst @@ -249,13 +249,15 @@ The following parameters are intentionally disallowed by design: only types, never over values. 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, we should likely disallow literal infinity and literal NaN. - -- Any: e.g. ``Literal[Any]`` Note: the semantics of what exactly - ``Literal[Any]`` means would need to be clarified first. +- Floats: e.g. ``Literal[3.14]``. Representing Literals of infinity or 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]``. ``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 --------------------- @@ -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 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 ============== @@ -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 `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 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 -final ``else`` statement in the following function is unreachable:: +enums. For example, the type checker should be capable of inferring that +the final ``else`` statement must be of type ``str``, since all three +values of the ``Status`` enum have already been exhausted:: class Status(Enum): SUCCESS = 0 INVALID_DATA = 1 FATAL_ERROR = 2 - def parse_status(s: Status) -> None: + def parse_status(s: Union[str, Status]) -> None: if s is Status.SUCCESS: print("Success!") 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: print("Unexpected fatal error...") else: - # Error should not be reported by type checkers that - # ignore errors in unreachable blocks - print("Nonsense" + 100) + # 's' must be of type 'str' since all other options are exhausted + print("Got custom status: " + s) -This behavior is technically not new: this behavior is +The interaction described above is not new: it's already `already codified within PEP 484 `_. However, many type -checkers (such as mypy) do not yet implement this behavior. Once Literal -types are introduced, it will become easier to do so: we can model -enums as being approximately equal to the union of their values and -take advantage of any existing logic regarding unions, exhaustibility, -and type narrowing. +checkers (such as mypy) do not yet implement this due to the expected +complexity of the implementation work. -So here, ``Status`` could be treated as being approximately equal to -``Literal[Status.SUCCESS, Status.INVALID_DATA, Status.FATAL_ERROR]`` +Some of this complexity will be alleviated once Literal types are introduced: +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. -Type checkers may optionally perform additional analysis and narrowing -beyond what is described above. +Interactions with narrowing +--------------------------- + +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 containment or equality checks:: @@ -566,7 +554,7 @@ containment or equality checks:: # Literal["MALFORMED", "ABORTED"] here. 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": expects_pending_status(status) @@ -577,7 +565,7 @@ involving Literal bools. For example, we can combine ``Literal[True]``, @overload def is_int_like(x: Union[int, List[int]]) -> Literal[True]: ... @overload - def is_int_like(x: Union[str, List[str]]) -> Literal[False]: ... + def is_int_like(x: object) -> bool: ... def is_int_like(x): ... vector: List[int] = [1, 2, 3]