PEP 586: Literal Types (#928)
This commit is contained in:
parent
375db992d5
commit
b870ad2a45
|
@ -0,0 +1,737 @@
|
||||||
|
PEP: 586
|
||||||
|
Title: Literal Types
|
||||||
|
Author: Michael Lee <michael.lee.0x2a@gmail.com>, Ivan Levkivskyi <levkivskyi@gmail.com>, Jukka Lehtosalo <jukka.lehtosalo@iki.fi>
|
||||||
|
Discussions-To: Typing-Sig <typing-sig@python.org>
|
||||||
|
Status: Draft
|
||||||
|
Type: Standards Track
|
||||||
|
Content-Type: text/x-rst
|
||||||
|
Created: 14-Mar-2018
|
||||||
|
Python-Version: 3.8
|
||||||
|
Post-History: 14-Mar-2018
|
||||||
|
|
||||||
|
Abstract
|
||||||
|
========
|
||||||
|
|
||||||
|
This PEP proposes adding *Literal types* to the PEP 484 ecosystem.
|
||||||
|
Literal types indicate that some expression has literally a
|
||||||
|
specific value. For example, the following function will accept
|
||||||
|
only expressions that have literally the value "4"::
|
||||||
|
|
||||||
|
from typing import Literal
|
||||||
|
|
||||||
|
def accepts_only_four(x: Literal[4]) -> None:
|
||||||
|
pass
|
||||||
|
|
||||||
|
accepts_only_four(4) # Ok
|
||||||
|
accepts_only_four(19) # Rejected
|
||||||
|
|
||||||
|
Motivation and Rationale
|
||||||
|
========================
|
||||||
|
|
||||||
|
Python has many APIs that return different types depending on the
|
||||||
|
value of some argument provided. For example:
|
||||||
|
|
||||||
|
- ``open(filename, mode)`` returns either ``IO[bytes]`` or ``IO[Text]``
|
||||||
|
depending on whether the second argument is something like ``r`` or
|
||||||
|
``rb``.
|
||||||
|
- ``subprocess.check_output(...)`` returns either bytes or text
|
||||||
|
depending on whether the ``universal_newlines`` keyword argument is
|
||||||
|
set to ``True`` or not.
|
||||||
|
|
||||||
|
This pattern is also fairly common in many popular 3rd party libraries.
|
||||||
|
For example, here are just two examples from pandas and numpy respectively:
|
||||||
|
|
||||||
|
- ``pandas.concat(...)`` will return either ``Series`` or
|
||||||
|
``DataFrame`` depending on whether the ``axis`` argument is set to
|
||||||
|
0 or 1.
|
||||||
|
|
||||||
|
- ``numpy.unique`` will return either a single array or a tuple containing
|
||||||
|
anywhere from two to four arrays depending on three boolean flag values.
|
||||||
|
|
||||||
|
The typing issue tracker contains some
|
||||||
|
`additional examples and discussion <typing-discussion_>`_.
|
||||||
|
|
||||||
|
There is currently no way of expressing the type signatures of these
|
||||||
|
functions: PEP 484 does not include any mechanism for writing signatures
|
||||||
|
where the return type varies depending on the value passed in.
|
||||||
|
Note that this problem persists even if we redesign these APIs to
|
||||||
|
instead accept enums: ``MyEnum.FOO`` and ``MyEnum.BAR`` are both
|
||||||
|
considered to be of type ``MyEnum``.
|
||||||
|
|
||||||
|
Currently, type checkers work around this limitation by adding ad-hoc
|
||||||
|
extensions for important builtins and standard library functions. For
|
||||||
|
example mypy comes bundled with a plugin that attempts to infer more
|
||||||
|
precise types for ``open(...)``. While this approach works for standard
|
||||||
|
library functions, it’s unsustainable in general: it’s not reasonable to
|
||||||
|
expect 3rd party library authors to maintain plugins for N different
|
||||||
|
type checkers.
|
||||||
|
|
||||||
|
We propose adding *Literal types* to address these gaps.
|
||||||
|
|
||||||
|
Core Semantics
|
||||||
|
==============
|
||||||
|
|
||||||
|
This section outlines the baseline behavior of literal types.
|
||||||
|
|
||||||
|
Core behavior
|
||||||
|
-------------
|
||||||
|
|
||||||
|
Literal types indicate a variable has a specific and
|
||||||
|
concrete value. For example, if we define some variable ``foo`` to have
|
||||||
|
type ``Literal[3]``, we are declaring that ``foo`` must be exactly equal
|
||||||
|
to ``3`` and no other value.
|
||||||
|
|
||||||
|
Given some value ``v`` that is a member of type ``T``, the type
|
||||||
|
``Literal[v]`` shall be treated as a subtype of ``T``. For example,
|
||||||
|
``Literal[3]`` is a subtype of ``int``.
|
||||||
|
|
||||||
|
All methods from the parent type will be directly inherited by the
|
||||||
|
literal type. So, if we have some variable ``foo`` of type ``Literal[3]``
|
||||||
|
it’s safe to do things like ``foo + 5`` since ``foo`` inherits int’s
|
||||||
|
``__add__`` method. The resulting type of ``foo + 5`` is ``int``.
|
||||||
|
|
||||||
|
This "inheriting" behavior is identical to how we
|
||||||
|
`handle NewTypes. <newtypes_>`_.
|
||||||
|
|
||||||
|
Equivalence of two Literals
|
||||||
|
---------------------------
|
||||||
|
|
||||||
|
Two types ``Literal[v1]`` and ``Literal[v2]`` are equivalent when
|
||||||
|
both of the following conditions are true:
|
||||||
|
|
||||||
|
1. ``type(v1) == type(v2)``
|
||||||
|
2. ``v1 == v2``
|
||||||
|
|
||||||
|
For example, ``Literal[20]`` and ``Literal[0x14]`` are equivalent.
|
||||||
|
However, ``Literal[0]`` and ``Literal[False]`` is *not* equivalent
|
||||||
|
despite that ``0 == False`` evaluates to 'true' at runtime: ``0``
|
||||||
|
has type ``int`` and ``False`` has type ``bool``.
|
||||||
|
|
||||||
|
Shortening unions of literals
|
||||||
|
-----------------------------
|
||||||
|
|
||||||
|
Literals are parameterized with one or more values. When a Literal is
|
||||||
|
parameterized with more than one value, it's treated as exactly equivalent
|
||||||
|
to the union of those types. That is, ``Literal[v1, v2, v3]`` is equivalent
|
||||||
|
to ``Union[Literal[v1], Literal[v2], Literal[v3]]``.
|
||||||
|
|
||||||
|
This shortcut helps make writing signatures for functions that accept
|
||||||
|
many different literals more ergonomic — for example, functions like
|
||||||
|
``open(...)``::
|
||||||
|
|
||||||
|
# Note: this is a simplification of the true type signature.
|
||||||
|
_PathType = Union[str, bytes, int]
|
||||||
|
|
||||||
|
@overload
|
||||||
|
def open(path: _PathType,
|
||||||
|
mode: Literal["r", "w", "a", "x", "r+", "w+", "a+", "x+"],
|
||||||
|
) -> IO[Text]: ...
|
||||||
|
@overload
|
||||||
|
def open(path: _PathType,
|
||||||
|
mode: Literal["rb", "wb", "ab", "xb", "r+b", "w+b", "a+b", "x+b"],
|
||||||
|
) -> IO[bytes]: ...
|
||||||
|
|
||||||
|
# Fallback overload for when the user isn't using literal types
|
||||||
|
@overload
|
||||||
|
def open(path: _PathType, mode: str) -> IO[Any]: ...
|
||||||
|
|
||||||
|
The provided values do not all have to be members of the same type.
|
||||||
|
For example, ``Literal[42, "foo", True]`` is a legal type.
|
||||||
|
|
||||||
|
However, Literal **must** be parameterized with at least one type.
|
||||||
|
Types like ``Literal[]`` or ``Literal`` are illegal.
|
||||||
|
|
||||||
|
|
||||||
|
Legal and illegal parameterizations
|
||||||
|
===================================
|
||||||
|
|
||||||
|
This section describes what exactly constitutes a legal ``Literal[...]`` type:
|
||||||
|
what values may and may not be used as parameters.
|
||||||
|
|
||||||
|
In short, a ``Literal[...]`` type may be parameterized by one or more literal
|
||||||
|
expressions, and nothing else.
|
||||||
|
|
||||||
|
|
||||||
|
Legal parameters for ``Literal`` at type check time
|
||||||
|
---------------------------------------------------
|
||||||
|
|
||||||
|
``Literal`` may be parameterized with literal ints, byte and unicode strings,
|
||||||
|
bools, Enum values and ``None``. So for example, all of
|
||||||
|
the following would be legal::
|
||||||
|
|
||||||
|
Literal[26]
|
||||||
|
Literal[0x1A] # Exactly equivalent to Literal[26]
|
||||||
|
Literal[-4]
|
||||||
|
Literal["hello world"]
|
||||||
|
Literal[b"hello world"]
|
||||||
|
Literal[u"hello world"]
|
||||||
|
Literal[True]
|
||||||
|
Literal[Color.RED] # Assuming Color is some enum
|
||||||
|
Literal[None]
|
||||||
|
|
||||||
|
**Note:** Since the type ``None`` is inhabited by just a single
|
||||||
|
value, the types ``None`` and ``Literal[None]`` are exactly equivalent.
|
||||||
|
Type checkers may simplify ``Literal[None]`` into just ``None``.
|
||||||
|
|
||||||
|
``Literal`` may also be parameterized by other literal types, or type aliases
|
||||||
|
to other literal types. For example, the following is legal::
|
||||||
|
|
||||||
|
ReadOnlyMode = Literal["r", "r+"]
|
||||||
|
WriteAndTruncateMode = Literal["w", "w+", "wt", "w+t"]
|
||||||
|
WriteNoTruncateMode = Literal["r+", "r+t"]
|
||||||
|
AppendMode = Literal["a", "a+", "at", "a+t"]
|
||||||
|
|
||||||
|
AllModes = Literal[ReadOnlyMode, WriteAndTruncateMode,
|
||||||
|
WriteNoTruncateMode, AppendMode]
|
||||||
|
|
||||||
|
This feature is again intended to help make using and reusing literal types
|
||||||
|
more ergonomic.
|
||||||
|
|
||||||
|
**Note:** As a consequence of the above rules, type checkers are also expected
|
||||||
|
to support types that look like the following::
|
||||||
|
|
||||||
|
Literal[Literal[Literal[1, 2, 3], "foo"], 5, None]
|
||||||
|
|
||||||
|
This should be exactly equivalent to the following type::
|
||||||
|
|
||||||
|
Literal[1, 2, 3, "foo", 5, None]
|
||||||
|
|
||||||
|
...and also to the following type::
|
||||||
|
|
||||||
|
Optional[Literal[1, 2, 3, "foo", 5]]
|
||||||
|
|
||||||
|
**Note:** String literal types like ``Literal["foo"]`` should subtype either
|
||||||
|
bytes or unicode in the same way regular string literals do at runtime.
|
||||||
|
|
||||||
|
For example, in Python 3, the type ``Literal["foo"]`` is equivalent to
|
||||||
|
``Literal[u"foo"]``, since ``"foo"`` is equivalent to ``u"foo"`` in Python 3.
|
||||||
|
|
||||||
|
Similarly, in Python 2, the type ``Literal["foo"]`` is equivalent to
|
||||||
|
``Literal[b"foo"]`` -- unless the file includes a
|
||||||
|
``from __future__ import unicode_literals`` import, in which case it would be
|
||||||
|
equivalent to ``Literal[u"foo"]``.
|
||||||
|
|
||||||
|
Illegal parameters for ``Literal`` at type check time
|
||||||
|
-----------------------------------------------------
|
||||||
|
|
||||||
|
The following parameters are intentionally disallowed by design:
|
||||||
|
|
||||||
|
- Arbitrary expressions like ``Literal[3 + 4]`` or
|
||||||
|
``Literal["foo".replace("o", "b")]``.
|
||||||
|
|
||||||
|
- Rationale: Literal types are meant to be a
|
||||||
|
minimal extension to the PEP 484 typing ecosystem and requiring type
|
||||||
|
checkers to interpret potentially expressions inside types adds too
|
||||||
|
much complexity. Also see `Rejected or out-of-scope ideas`_.
|
||||||
|
|
||||||
|
- As a consequence, complex numbers like ``Literal[4 + 3j]`` and
|
||||||
|
``Literal[-4 + 2j]`` are also prohibited. For consistency, literals like
|
||||||
|
``Literal[4j]`` that contain just a single complex number are also
|
||||||
|
prohibited.
|
||||||
|
|
||||||
|
- The only exception to this rule is the unary ``-`` (minus) for ints: types
|
||||||
|
like ``Literal[-5]`` are *accepted*.
|
||||||
|
|
||||||
|
- Tuples containing valid literal types like ``Literal[(1, "foo", "bar")]``.
|
||||||
|
The user could always express this type as
|
||||||
|
``Tuple[Literal[1], Literal["foo"], Literal["bar"]]`` instead. Also,
|
||||||
|
tuples are likely to be confused with the ``Literal[1, 2, 3]``
|
||||||
|
shortcut.
|
||||||
|
|
||||||
|
- Mutable literal data structures like dict literals, list literals, or
|
||||||
|
set literals: literals are always implicitly final and immutable. So,
|
||||||
|
``Literal[{"a": "b", "c": "d"}]`` is illegal.
|
||||||
|
|
||||||
|
- Any other types: for example, ``Literal[Path]``, or
|
||||||
|
``Literal[some_object_instance]`` are illegal. This includes typevars: if
|
||||||
|
``T`` is a typevar, ``Literal[T]`` is not allowed. Typevars can vary over
|
||||||
|
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.
|
||||||
|
|
||||||
|
- 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.
|
||||||
|
|
||||||
|
Parameters at runtime
|
||||||
|
---------------------
|
||||||
|
|
||||||
|
Although the set of parameters ``Literal[...]`` may contain at type check time
|
||||||
|
is very small, the actual implementation of ``typing.Literal`` will not perform
|
||||||
|
any checks at runtime. For example::
|
||||||
|
|
||||||
|
def my_function(x: Literal[1 + 2]) -> int:
|
||||||
|
return x * 3
|
||||||
|
|
||||||
|
x: Literal = 3
|
||||||
|
y: Literal[my_function] = my_function
|
||||||
|
|
||||||
|
The type checker should reject this program: all three uses of
|
||||||
|
``Literal`` are *invalid* according to this spec. However, Python itself
|
||||||
|
should execute this program with no errors.
|
||||||
|
|
||||||
|
This is partly to help us preserve flexibility in case we want to expand the
|
||||||
|
scope of what ``Literal`` can be used for in the future, and partly because
|
||||||
|
it is not possible to detect all illegal parameters at runtime to begin with.
|
||||||
|
For example, it is impossible to distinguish between ``Literal[1 + 2]`` and
|
||||||
|
``Literal[3]`` at runtime.
|
||||||
|
|
||||||
|
Literals, enums, and forward references
|
||||||
|
---------------------------------------
|
||||||
|
|
||||||
|
One potential ambiguity is between literal strings and forward
|
||||||
|
references to literal enum members. For example, suppose we have the
|
||||||
|
type ``Literal["Color.RED"]``. Does this literal type
|
||||||
|
contain a string literal or a forward reference to some ``Color.RED``
|
||||||
|
enum member?
|
||||||
|
|
||||||
|
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
|
||||||
|
==============
|
||||||
|
|
||||||
|
This section describes a few rules regarding type inference and
|
||||||
|
literals, along with some examples.
|
||||||
|
|
||||||
|
Backwards compatibility
|
||||||
|
-----------------------
|
||||||
|
|
||||||
|
When type checkers add support for Literal, it's important they do so
|
||||||
|
in a way that preserves backwards-compatibility. Code that used to
|
||||||
|
type check **must** continue to type check after support for Literal
|
||||||
|
is added.
|
||||||
|
|
||||||
|
This is particularly important when performing type inference. For
|
||||||
|
example, given the statement ``x = "blue"``, should the inferred
|
||||||
|
type of ``x`` be ``str`` or ``Literal["blue"]``?
|
||||||
|
|
||||||
|
This PEP does not require any particular strategy for cases like this,
|
||||||
|
apart from requiring that backwards compatibility is maintained.
|
||||||
|
|
||||||
|
For example, one simple strategy for meeting this requirement would be
|
||||||
|
to always assume expressions are *not* Literal types unless they are
|
||||||
|
explicitly annotated otherwise. A type checker using this strategy would
|
||||||
|
always infer that ``x`` is of type ``str`` in the above example.
|
||||||
|
|
||||||
|
If type checkers choose to use more sophisticated inference strategies,
|
||||||
|
they should avoid being too over-zealous while doing so.
|
||||||
|
|
||||||
|
For example, one strategy that does *not* work is always assuming expressions
|
||||||
|
are Literal types. This naive strategy would cause programs like the
|
||||||
|
following to start failing when they previously did not::
|
||||||
|
|
||||||
|
# If a type checker infers 'var' has type Literal[3]
|
||||||
|
# and my_list has type List[Literal[3]]...
|
||||||
|
var = 3
|
||||||
|
my_list = [var]
|
||||||
|
|
||||||
|
# ...this call would be a type-error.
|
||||||
|
my_list.append(4)
|
||||||
|
|
||||||
|
Another example of when this strategy would fail is when setting fields
|
||||||
|
in objects::
|
||||||
|
|
||||||
|
class MyObject:
|
||||||
|
def __init__(self) -> None:
|
||||||
|
# If a type checker infers MyObject.field has type Literal[3]...
|
||||||
|
self.field = 3
|
||||||
|
|
||||||
|
m = MyObject()
|
||||||
|
|
||||||
|
# ...this assignment would no longer type check
|
||||||
|
m.field = 4
|
||||||
|
|
||||||
|
Using non-Literals in Literal contexts
|
||||||
|
--------------------------------------
|
||||||
|
|
||||||
|
Literal types follow the existing rules regarding subtyping with no additional
|
||||||
|
special-casing. For example, programs like the following are type safe::
|
||||||
|
|
||||||
|
def expects_str(x: str) -> None: ...
|
||||||
|
var: Literal["foo"] = "foo"
|
||||||
|
|
||||||
|
# Legal: Literal["foo"] is a subtype of str
|
||||||
|
expects_str(var)
|
||||||
|
|
||||||
|
This also means non-Literal expressions in general should not automatically
|
||||||
|
inferred to be Literal. For example::
|
||||||
|
|
||||||
|
def expects_literal(x: Literal["foo"]) -> None: ...
|
||||||
|
|
||||||
|
def runner(my_str: str) -> None:
|
||||||
|
# ILLEGAL: str is not a subclass of Literal["foo"]
|
||||||
|
expects_literal(my_str)
|
||||||
|
|
||||||
|
**Note:** If the user wants their API to support accepting both literals
|
||||||
|
*and* the original type -- perhaps for legacy purposes -- they should
|
||||||
|
implement a fallback overload. See `Interactions with overloads`_.
|
||||||
|
|
||||||
|
Interactions with other types and features
|
||||||
|
==========================================
|
||||||
|
|
||||||
|
This section discusses how Literal types interact with other existing types.
|
||||||
|
|
||||||
|
Intelligent indexing of structured data
|
||||||
|
---------------------------------------
|
||||||
|
|
||||||
|
Literals can be used to "intelligently index" into structured types like
|
||||||
|
tuples, NamedTuple, and classes. (Note: this is not an exhaustive list).
|
||||||
|
|
||||||
|
For example, type checkers should infer the correct value type when
|
||||||
|
indexing into a tuple using an int key that corresponds a valid index::
|
||||||
|
|
||||||
|
a: Literal[0] = 0
|
||||||
|
b: Literal[5] = 5
|
||||||
|
|
||||||
|
some_tuple: Tuple[int, str, List[bool]] = (3, "abc", [True, False])
|
||||||
|
reveal_type(some_tuple[a]) # Revealed type is 'int'
|
||||||
|
some_tuple[b] # Error: 5 is not a valid index into the tuple
|
||||||
|
|
||||||
|
We expect similar behavior when using functions like getattr::
|
||||||
|
|
||||||
|
class Test:
|
||||||
|
def __init__(self, param: int) -> None:
|
||||||
|
self.myfield = param
|
||||||
|
|
||||||
|
def mymethod(self, val: int) -> str: ...
|
||||||
|
|
||||||
|
a: Literal["myfield"] = "myfield"
|
||||||
|
b: Literal["mymethod"] = "mymethod"
|
||||||
|
c: Literal["blah"] = "blah"
|
||||||
|
|
||||||
|
t = Test()
|
||||||
|
reveal_type(getattr(t, a)) # Revealed type is 'int'
|
||||||
|
reveal_type(getattr(t, b)) # Revealed type is 'Callable[[int], str]'
|
||||||
|
getattr(t, c) # Error: No attribute named 'blah' in Test
|
||||||
|
|
||||||
|
Interactions with overloads
|
||||||
|
---------------------------
|
||||||
|
|
||||||
|
Literal types and overloads do not need to interact in a special
|
||||||
|
way: the existing rules work fine.
|
||||||
|
|
||||||
|
However, one important use case type checkers must take care to
|
||||||
|
support is the ability to use a *fallback* when the user is not using literal
|
||||||
|
types. For example, consider ``open``::
|
||||||
|
|
||||||
|
_PathType = Union[str, bytes, int]
|
||||||
|
|
||||||
|
@overload
|
||||||
|
def open(path: _PathType,
|
||||||
|
mode: Literal["r", "w", "a", "x", "r+", "w+", "a+", "x+"],
|
||||||
|
) -> IO[Text]: ...
|
||||||
|
@overload
|
||||||
|
def open(path: _PathType,
|
||||||
|
mode: Literal["rb", "wb", "ab", "xb", "r+b", "w+b", "a+b", "x+b"],
|
||||||
|
) -> IO[bytes]: ...
|
||||||
|
|
||||||
|
# Fallback overload for when the user isn't using literal types
|
||||||
|
@overload
|
||||||
|
def open(path: _PathType, mode: str) -> IO[Any]: ...
|
||||||
|
|
||||||
|
If we change the signature of ``open`` to use just the first two overloads,
|
||||||
|
we would break any code that does not pass in a literal string expression.
|
||||||
|
For example, code like this would be broken::
|
||||||
|
|
||||||
|
mode: str = pick_file_mode(...)
|
||||||
|
with open(path, mode) as f:
|
||||||
|
# f should continue to be of type IO[Any] here
|
||||||
|
|
||||||
|
A little more broadly: we propose adding a policy to typeshed that
|
||||||
|
mandates that whenever we add literal types to some existing API, we also
|
||||||
|
always include a fallback overload to maintain backwards-compatibility.
|
||||||
|
|
||||||
|
Interactions with generics
|
||||||
|
--------------------------
|
||||||
|
|
||||||
|
Types like ``Literal[3]`` are meant to be just plain old subclasses of
|
||||||
|
``int``. This means you can use types like ``Literal[3]`` anywhere
|
||||||
|
you could use normal types, such as with generics.
|
||||||
|
|
||||||
|
This means that it is legal to parameterize generic functions or
|
||||||
|
classes using Literal types::
|
||||||
|
|
||||||
|
A = TypeVar('A', bound=int)
|
||||||
|
B = TypeVar('B', bound=int)
|
||||||
|
C = TypeVar('C', bound=int)
|
||||||
|
|
||||||
|
# A simplified definition for Matrix[row, column]
|
||||||
|
class Matrix(Generic[A, B]):
|
||||||
|
def __add__(self, other: Matrix[A, B]) -> Matrix[A, B]: ...
|
||||||
|
def __matmul__(self, other: Matrix[B, C]) -> Matrix[A, C]: ...
|
||||||
|
def transpose(self) -> Matrix[B, A]: ...
|
||||||
|
|
||||||
|
foo: Matrix[Literal[2], Literal[3]] = Matrix(...)
|
||||||
|
bar: Matrix[Literal[3], Literal[7]] = Matrix(...)
|
||||||
|
|
||||||
|
baz = foo @ bar
|
||||||
|
reveal_type(baz) # Revealed type is 'Matrix[Literal[2], Literal[7]]'
|
||||||
|
|
||||||
|
Similarly, it is legal to construct TypeVars with value restrictions
|
||||||
|
or bounds involving Literal types::
|
||||||
|
|
||||||
|
T = TypeVar('T', Literal["a"], Literal["b"], Literal["c"])
|
||||||
|
S = TypeVar('S', bound=Literal["foo"])
|
||||||
|
|
||||||
|
...although it is unclear when it would ever be useful to construct a
|
||||||
|
TypeVar with a Literal upper bound. For example, the ``S`` TypeVar in
|
||||||
|
the above example is essentially pointless: we can get equivalent behavior
|
||||||
|
by using ``S = Literal["foo"]`` instead.
|
||||||
|
|
||||||
|
**Note:** Literal types and generics deliberately interact in only very
|
||||||
|
basic and limited ways. In particular, libraries that want to typecheck
|
||||||
|
code containing an heavy amount of numeric or numpy-style manipulation will
|
||||||
|
almost certainly likely find Literal types as proposed in this PEP to be
|
||||||
|
insufficient for their needs.
|
||||||
|
|
||||||
|
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
|
||||||
|
--------------------------------
|
||||||
|
|
||||||
|
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::
|
||||||
|
|
||||||
|
class Status(Enum):
|
||||||
|
SUCCESS = 0
|
||||||
|
INVALID_DATA = 1
|
||||||
|
FATAL_ERROR = 2
|
||||||
|
|
||||||
|
def parse_status(s: Status) -> None:
|
||||||
|
if s is Status.SUCCESS:
|
||||||
|
print("Success!")
|
||||||
|
elif s is Status.INVALID_DATA:
|
||||||
|
print("The given data is invalid because...")
|
||||||
|
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)
|
||||||
|
|
||||||
|
This behavior is technically not new: this behavior is
|
||||||
|
`already codified within PEP 484 <pep-484-enums_>`_. 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.
|
||||||
|
|
||||||
|
So here, ``Status`` could be treated as being approximately equal 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.
|
||||||
|
|
||||||
|
For example, it may be useful to perform narrowing based on things like
|
||||||
|
containment or equality checks::
|
||||||
|
|
||||||
|
def parse_status(status: str) -> None:
|
||||||
|
if status in ("MALFORMED", "ABORTED"):
|
||||||
|
# Type checker could narrow 'status' to type
|
||||||
|
# Literal["MALFORMED", "ABORTED"] here.
|
||||||
|
return expects_bad_status(status)
|
||||||
|
|
||||||
|
# Similarly, type checker could narrow 'x' to Literal["PENDING"]
|
||||||
|
if status == "PENDING":
|
||||||
|
expects_pending_status(status)
|
||||||
|
|
||||||
|
It may also be useful to perform narrowing taking into account expressions
|
||||||
|
involving Literal bools. For example, we can combine ``Literal[True]``,
|
||||||
|
``Literal[False]``, and overloads to construct "custom type guards"::
|
||||||
|
|
||||||
|
@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): ...
|
||||||
|
|
||||||
|
vector: List[int] = [1, 2, 3]
|
||||||
|
if is_int_like(vector):
|
||||||
|
vector.append(3)
|
||||||
|
else:
|
||||||
|
vector.append("bad") # This branch is inferred to be unreachable
|
||||||
|
|
||||||
|
scalar: Union[int, str]
|
||||||
|
if is_int_like(scalar):
|
||||||
|
scalar += 3 # Type checks: type of 'scalar' is narrowed to 'int'
|
||||||
|
else:
|
||||||
|
scalar += "foo" # Type checks: type of 'scalar' is narrowed to 'str'
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
Rejected or out-of-scope ideas
|
||||||
|
==============================
|
||||||
|
|
||||||
|
This section outlines some potential features that are explicitly out-of-scope.
|
||||||
|
|
||||||
|
True dependent types/integer generics
|
||||||
|
-------------------------------------
|
||||||
|
|
||||||
|
This proposal is essentially describing adding a very simplified
|
||||||
|
dependent type system to the PEP 484 ecosystem. One obvious extension
|
||||||
|
is to implement a full-fledged dependent type system that let users
|
||||||
|
predicate types based on their values in arbitrary ways. This would
|
||||||
|
let us write signatures like the below::
|
||||||
|
|
||||||
|
# A vector has length 'n', containing elements of type 'T'
|
||||||
|
class Vector(Generic[N, T]): ...
|
||||||
|
|
||||||
|
# The type checker will statically verify our function genuinely does
|
||||||
|
# construct a vector that is equal in length to "len(vec1) + len(vec2)"
|
||||||
|
# and will throw an error if it does not.
|
||||||
|
def concat(vec1: Vector[A, T], vec2: Vector[B, T]) -> Vector[A + B, T]:
|
||||||
|
# ...snip...
|
||||||
|
|
||||||
|
At the very least, it would be useful to add some form of integer generics.
|
||||||
|
|
||||||
|
Although such a type system would certainly be useful, it’s out-of-scope
|
||||||
|
for this PEP: it would require a far more substantial amount of implementation
|
||||||
|
work, discussion, and research to complete compared to the current proposal.
|
||||||
|
|
||||||
|
It's entirely possible we'll circle back and revisit this topic in the future:
|
||||||
|
we very likely will need some form of dependent typing along with other
|
||||||
|
extensions like variadic generics to support popular libraries like numpy.
|
||||||
|
|
||||||
|
This PEP should be seen as a stepping stones towards this goal,
|
||||||
|
rather then an attempt at providing a comprehensive solution.
|
||||||
|
|
||||||
|
Adding more concise syntax
|
||||||
|
--------------------------
|
||||||
|
|
||||||
|
One objection to this PEP is that having to explicitly write ``Literal[...]``
|
||||||
|
feels verbose. For example, instead of writing::
|
||||||
|
|
||||||
|
def foobar(arg1: Literal[1], arg2: Literal[True]) -> None:
|
||||||
|
pass
|
||||||
|
|
||||||
|
...it would be nice to instead write::
|
||||||
|
|
||||||
|
def foobar(arg1: 1, arg2: True) -> None:
|
||||||
|
pass
|
||||||
|
|
||||||
|
Unfortunately, these abbreviations simply will not work with the
|
||||||
|
existing implementation of ``typing`` at runtime. For example, the
|
||||||
|
following snippet crashes when run using Python 3.7::
|
||||||
|
|
||||||
|
from typing import Tuple
|
||||||
|
|
||||||
|
# Supposed to accept tuple containing the literals 1 and 2
|
||||||
|
def foo(x: Tuple[1, 2]) -> None:
|
||||||
|
pass
|
||||||
|
|
||||||
|
Running this yields the following exception::
|
||||||
|
|
||||||
|
TypeError: Tuple[t0, t1, ...]: each t must be a type. Got 1.
|
||||||
|
|
||||||
|
We don’t want users to have to memorize exactly when it’s ok to elide
|
||||||
|
``Literal``, so we require ``Literal`` to always be present.
|
||||||
|
|
||||||
|
A little more broadly, we feel overhauling the syntax of types in
|
||||||
|
Python is not within the scope of this PEP: it would be best to have
|
||||||
|
that discussion in a separate PEP, instead of attaching it to this one.
|
||||||
|
So, this PEP deliberately does not try and innovate Python's type syntax.
|
||||||
|
|
||||||
|
Backporting the ``Literal`` type
|
||||||
|
================================
|
||||||
|
|
||||||
|
Once this PEP is accepted, the ``Literal`` type will need to be backported for
|
||||||
|
Python versions that come bundled with older versions of the ``typing`` module.
|
||||||
|
We plan to do this by adding ``Literal`` to the ``typing_extensions`` 3rd party
|
||||||
|
module, which contains a variety of other backported types.
|
||||||
|
|
||||||
|
Implementation
|
||||||
|
==============
|
||||||
|
|
||||||
|
The mypy type checker currently has implemented a large subset of the behavior
|
||||||
|
described in this spec, with the exception of enum Literals and some of the
|
||||||
|
more complex narrowing interactions described above.
|
||||||
|
|
||||||
|
|
||||||
|
Related work
|
||||||
|
============
|
||||||
|
|
||||||
|
This proposal was written based on the discussion that took place in the
|
||||||
|
following threads:
|
||||||
|
|
||||||
|
- `Check that literals belong to/are excluded from a set of values <typing-discussion_>`_
|
||||||
|
|
||||||
|
- `Simple dependent types <mypy-discussion_>`_
|
||||||
|
|
||||||
|
- `Typing for multi-dimensional arrays <arrays-discussion_>`_
|
||||||
|
|
||||||
|
The overall design of this proposal also ended up converging into
|
||||||
|
something similar to how
|
||||||
|
`literal types are handled in TypeScript <typescript-literal-types_>`_.
|
||||||
|
|
||||||
|
.. _typing-discussion: https://github.com/python/typing/issues/478
|
||||||
|
|
||||||
|
.. _mypy-discussion: https://github.com/python/mypy/issues/3062
|
||||||
|
|
||||||
|
.. _arrays-discussion: https://github.com/python/typing/issues/513
|
||||||
|
|
||||||
|
.. _typescript-literal-types: https://www.typescriptlang.org/docs/handbook/advanced-types.html#string-literal_types
|
||||||
|
|
||||||
|
.. _typescript-index-types: https://www.typescriptlang.org/docs/handbook/advanced-types.html#index-types
|
||||||
|
|
||||||
|
.. _newtypes: https://www.python.org/dev/peps/pep-0484/#newtype-helper-function
|
||||||
|
|
||||||
|
.. _pep-484-enums: https://www.python.org/dev/peps/pep-0484/#support-for-singleton-types-in-unions
|
||||||
|
|
||||||
|
|
||||||
|
Acknowledgements
|
||||||
|
================
|
||||||
|
|
||||||
|
Thanks to Mark Mendoza, Ran Benita, Rebecca Chen, and the other members of
|
||||||
|
typing-sig for their comments on this PEP.
|
||||||
|
|
||||||
|
Additional thanks to the various participants in the mypy and typing issue
|
||||||
|
trackers, who helped provide a lot of the motivation and reasoning behind
|
||||||
|
this PEP.
|
||||||
|
|
||||||
|
|
||||||
|
Copyright
|
||||||
|
=========
|
||||||
|
|
||||||
|
This document has been placed in the public domain.
|
||||||
|
|
||||||
|
|
||||||
|
..
|
||||||
|
Local Variables:
|
||||||
|
mode: indented-text
|
||||||
|
indent-tabs-mode: nil
|
||||||
|
sentence-end-double-space: t
|
||||||
|
fill-column: 70
|
||||||
|
coding: utf-8
|
||||||
|
End:
|
||||||
|
|
Loading…
Reference in New Issue