Skip to content

Commit 7055725

Browse files
authored
Update docs for Literal types (#8152)
This pull request is a long-overdue update of the Literal type docs. It: 1. Removes the "this is alpha" warning we have at the top. 2. Mentions Literal enums are a thing (and works in a very brief example of one). 3. Adds a section about "intelligent indexing". 4. Adds a section with an example about the "tagged union" pattern (see #8151). 5. Cross-references the "tagged union" docs with the TypedDicts docs.
1 parent 3dce3fd commit 7055725

File tree

2 files changed

+154
-20
lines changed

2 files changed

+154
-20
lines changed

docs/source/literal_types.rst

Lines changed: 141 additions & 20 deletions
Original file line numberDiff line numberDiff line change
@@ -3,13 +3,6 @@
33
Literal types
44
=============
55

6-
.. note::
7-
8-
``Literal`` is an officially supported feature, but is highly experimental
9-
and should be considered to be in alpha stage. It is very likely that future
10-
releases of mypy will modify the behavior of literal types, either by adding
11-
new features or by tuning or removing problematic ones.
12-
136
Literal types let you indicate that an expression is equal to some specific
147
primitive value. For example, if we annotate a variable with type ``Literal["foo"]``,
158
mypy will understand that variable is not only of type ``str``, but is also
@@ -23,8 +16,7 @@ precise type signature for this function using ``Literal[...]`` and overloads:
2316

2417
.. code-block:: python
2518
26-
from typing import overload, Union
27-
from typing_extensions import Literal
19+
from typing import overload, Union, Literal
2820
2921
# The first two overloads use Literal[...] so we can
3022
# have precise return types:
@@ -53,18 +45,25 @@ precise type signature for this function using ``Literal[...]`` and overloads:
5345
variable = True
5446
reveal_type(fetch_data(variable)) # Revealed type is 'Union[bytes, str]'
5547
48+
.. note::
49+
50+
The examples in this page import ``Literal`` as well as ``Final`` and
51+
``TypedDict`` from the ``typing`` module. These types were added to
52+
``typing`` in Python 3.8, but are also available for use in Python 2.7
53+
and 3.4 - 3.7 via the ``typing_extensions`` package.
54+
5655
Parameterizing Literals
5756
***********************
5857

59-
Literal types may contain one or more literal bools, ints, strs, and bytes.
60-
However, literal types **cannot** contain arbitrary expressions:
58+
Literal types may contain one or more literal bools, ints, strs, bytes, and
59+
enum values. However, literal types **cannot** contain arbitrary expressions:
6160
types like ``Literal[my_string.trim()]``, ``Literal[x > 3]``, or ``Literal[3j + 4]``
6261
are all illegal.
6362

6463
Literals containing two or more values are equivalent to the union of those values.
65-
So, ``Literal[-3, b"foo", True]`` is equivalent to
66-
``Union[Literal[-3], Literal[b"foo"], Literal[True]]``. This makes writing
67-
more complex types involving literals a little more convenient.
64+
So, ``Literal[-3, b"foo", MyEnum.A]`` is equivalent to
65+
``Union[Literal[-3], Literal[b"foo"], Literal[MyEnum.A]]``. This makes writing more
66+
complex types involving literals a little more convenient.
6867

6968
Literal types may also contain ``None``. Mypy will treat ``Literal[None]`` as being
7069
equivalent to just ``None``. This means that ``Literal[4, None]``,
@@ -88,9 +87,6 @@ Literals may not contain any other kind of type or expression. This means doing
8887
``Literal[my_instance]``, ``Literal[Any]``, ``Literal[3.14]``, or
8988
``Literal[{"foo": 2, "bar": 5}]`` are all illegal.
9089

91-
Future versions of mypy may relax some of these restrictions. For example, we
92-
plan on adding support for using enum values inside ``Literal[...]`` in an upcoming release.
93-
9490
Declaring literal variables
9591
***************************
9692

@@ -115,7 +111,7 @@ you can instead change the variable to be ``Final`` (see :ref:`final_attrs`):
115111

116112
.. code-block:: python
117113
118-
from typing_extensions import Final, Literal
114+
from typing import Final, Literal
119115
120116
def expects_literal(x: Literal[19]) -> None: pass
121117
@@ -134,7 +130,7 @@ For example, mypy will type check the above program almost as if it were written
134130

135131
.. code-block:: python
136132
137-
from typing_extensions import Final, Literal
133+
from typing import Final, Literal
138134
139135
def expects_literal(x: Literal[19]) -> None: pass
140136
@@ -151,7 +147,7 @@ For example, compare and contrast what happens when you try appending these type
151147

152148
.. code-block:: python
153149
154-
from typing_extensions import Final, Literal
150+
from typing import Final, Literal
155151
156152
a: Final = 19
157153
b: Literal[19] = 19
@@ -168,6 +164,131 @@ For example, compare and contrast what happens when you try appending these type
168164
reveal_type(list_of_lits) # Revealed type is 'List[Literal[19]]'
169165
170166
167+
Intelligent indexing
168+
********************
169+
170+
We can use Literal types to more precisely index into structured heterogeneous
171+
types such as tuples, NamedTuples, and TypedDicts. This feature is known as
172+
*intelligent indexing*.
173+
174+
For example, when we index into a tuple using some int, the inferred type is
175+
normally the union of the tuple item types. However, if we want just the type
176+
corresponding to some particular index, we can use Literal types like so:
177+
178+
.. code-block:: python
179+
180+
from typing import TypedDict
181+
182+
tup = ("foo", 3.4)
183+
184+
# Indexing with an int literal gives us the exact type for that index
185+
reveal_type(tup[0]) # Revealed type is 'str'
186+
187+
# But what if we want the index to be a variable? Normally mypy won't
188+
# know exactly what the index is and so will return a less precise type:
189+
int_index = 1
190+
reveal_type(tup[int_index]) # Revealed type is 'Union[str, float]'
191+
192+
# But if we use either Literal types or a Final int, we can gain back
193+
# the precision we originally had:
194+
lit_index: Literal[1] = 1
195+
fin_index: Final = 1
196+
reveal_type(tup[lit_index]) # Revealed type is 'str'
197+
reveal_type(tup[fin_index]) # Revealed type is 'str'
198+
199+
# We can do the same thing with with TypedDict and str keys:
200+
class MyDict(TypedDict):
201+
name: str
202+
main_id: int
203+
backup_id: int
204+
205+
d: MyDict = {"name": "Saanvi", "main_id": 111, "backup_id": 222}
206+
name_key: Final = "name"
207+
reveal_type(d[name_key]) # Revealed type is 'str'
208+
209+
# You can also index using unions of literals
210+
id_key: Literal["main_id", "backup_id"]
211+
reveal_type(d[id_key]) # Revealed type is 'int'
212+
213+
.. _tagged_unions:
214+
215+
Tagged unions
216+
*************
217+
218+
When you have a union of types, you can normally discriminate between each type
219+
in the union by using ``isinstance`` checks. For example, if you had a variable ``x`` of
220+
type ``Union[int, str]``, you could write some code that runs only if ``x`` is an int
221+
by doing ``if isinstance(x, int): ...``.
222+
223+
However, it is not always possible or convenient to do this. For example, it is not
224+
possible to use ``isinstance`` to distinguish between two different TypedDicts since
225+
at runtime, your variable will simply be just a dict.
226+
227+
Instead, what you can do is *label* or *tag* your TypedDicts with a distinct Literal
228+
type. Then, you can discriminate between each kind of TypedDict by checking the label:
229+
230+
.. code-block:: python
231+
232+
from typing import Literal, TypedDict, Union
233+
234+
class NewJobEvent(TypedDict):
235+
tag: Literal["new-job"]
236+
job_name: str
237+
config_file_path: str
238+
239+
class CancelJobEvent(TypedDict):
240+
tag: Literal["cancel-job"]
241+
job_id: int
242+
243+
Event = Union[NewJobEvent, CancelJobEvent]
244+
245+
def process_event(event: Event) -> None:
246+
# Since we made sure both TypedDicts have a key named 'tag', it's
247+
# safe to do 'event["tag"]'. This expression normally has the type
248+
# Literal["new-job", "cancel-job"], but the check below will narrow
249+
# the type to either Literal["new-job"] or Literal["cancel-job"].
250+
#
251+
# This in turns narrows the type of 'event' to either NewJobEvent
252+
# or CancelJobEvent.
253+
if event["tag"] == "new-job":
254+
print(event["job_name"])
255+
else:
256+
print(event["job_id"])
257+
258+
While this feature is mostly useful when working with TypedDicts, you can also
259+
use the same technique wih regular objects, tuples, or namedtuples.
260+
261+
Similarly, tags do not need to be specifically str Literals: they can be any type
262+
you can normally narrow within ``if`` statements and the like. For example, you
263+
could have your tags be int or Enum Literals or even regular classes you narrow
264+
using ``isinstance()``:
265+
266+
.. code-block:: python
267+
268+
from typing import Generic, TypeVar, Union
269+
270+
T = TypeVar('T')
271+
272+
class Wrapper(Generic[T]):
273+
def __init__(self, inner: T) -> None:
274+
self.inner = inner
275+
276+
def process(w: Union[Wrapper[int], Wrapper[str]]) -> None:
277+
# Doing `if isinstance(w, Wrapper[int])` does not work: isinstance requires
278+
# that the second argument always be an *erased* type, with no generics.
279+
# This is because generics are a typing-only concept and do not exist at
280+
# runtime in a way `isinstance` can always check.
281+
#
282+
# However, we can side-step this by checking the type of `w.inner` to
283+
# narrow `w` itself:
284+
if isinstance(w.inner, int):
285+
reveal_type(w) # Revealed type is 'Wrapper[int]'
286+
else:
287+
reveal_type(w) # Revealed type is 'Wrapper[str]'
288+
289+
This feature is sometimes called "sum types" or "discriminated union types"
290+
in other programming languages.
291+
171292
Limitations
172293
***********
173294

docs/source/more_types.rst

Lines changed: 13 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1119,3 +1119,16 @@ and non-required keys, such as ``Movie`` above, will only be compatible with
11191119
another ``TypedDict`` if all required keys in the other ``TypedDict`` are required keys in the
11201120
first ``TypedDict``, and all non-required keys of the other ``TypedDict`` are also non-required keys
11211121
in the first ``TypedDict``.
1122+
1123+
Unions of TypedDicts
1124+
--------------------
1125+
1126+
Since TypedDicts are really just regular dicts at runtime, it is not possible to
1127+
use ``isinstance`` checks to distinguish between different variants of a Union of
1128+
TypedDict in the same way you can with regular objects.
1129+
1130+
Instead, you can use the :ref:`tagged union pattern <tagged_unions>`. The referenced
1131+
section of the docs has a full description with an example, but in short, you will
1132+
need to give each TypedDict the same key where each value has a unique
1133+
unique :ref:`Literal type <literal_types>`. Then, check that key to distinguish
1134+
between your TypedDicts.

0 commit comments

Comments
 (0)