Skip to content

Commit

Permalink
Update docs for Literal types (#8152)
Browse files Browse the repository at this point in the history
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.
  • Loading branch information
Michael0x2a authored Jan 8, 2020
1 parent 3dce3fd commit 7055725
Show file tree
Hide file tree
Showing 2 changed files with 154 additions and 20 deletions.
161 changes: 141 additions & 20 deletions docs/source/literal_types.rst
Original file line number Diff line number Diff line change
Expand Up @@ -3,13 +3,6 @@
Literal types
=============

.. note::

``Literal`` is an officially supported feature, but is highly experimental
and should be considered to be in alpha stage. It is very likely that future
releases of mypy will modify the behavior of literal types, either by adding
new features or by tuning or removing problematic ones.

Literal types let you indicate that an expression is equal to some specific
primitive value. For example, if we annotate a variable with type ``Literal["foo"]``,
mypy will understand that variable is not only of type ``str``, but is also
Expand All @@ -23,8 +16,7 @@ precise type signature for this function using ``Literal[...]`` and overloads:

.. code-block:: python
from typing import overload, Union
from typing_extensions import Literal
from typing import overload, Union, Literal
# The first two overloads use Literal[...] so we can
# have precise return types:
Expand Down Expand Up @@ -53,18 +45,25 @@ precise type signature for this function using ``Literal[...]`` and overloads:
variable = True
reveal_type(fetch_data(variable)) # Revealed type is 'Union[bytes, str]'
.. note::

The examples in this page import ``Literal`` as well as ``Final`` and
``TypedDict`` from the ``typing`` module. These types were added to
``typing`` in Python 3.8, but are also available for use in Python 2.7
and 3.4 - 3.7 via the ``typing_extensions`` package.

Parameterizing Literals
***********************

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

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

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

Future versions of mypy may relax some of these restrictions. For example, we
plan on adding support for using enum values inside ``Literal[...]`` in an upcoming release.

Declaring literal variables
***************************

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

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

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

.. code-block:: python
from typing_extensions import Final, Literal
from typing import Final, Literal
a: Final = 19
b: Literal[19] = 19
Expand All @@ -168,6 +164,131 @@ For example, compare and contrast what happens when you try appending these type
reveal_type(list_of_lits) # Revealed type is 'List[Literal[19]]'
Intelligent indexing
********************

We can use Literal types to more precisely index into structured heterogeneous
types such as tuples, NamedTuples, and TypedDicts. This feature is known as
*intelligent indexing*.

For example, when we index into a tuple using some int, the inferred type is
normally the union of the tuple item types. However, if we want just the type
corresponding to some particular index, we can use Literal types like so:

.. code-block:: python
from typing import TypedDict
tup = ("foo", 3.4)
# Indexing with an int literal gives us the exact type for that index
reveal_type(tup[0]) # Revealed type is 'str'
# But what if we want the index to be a variable? Normally mypy won't
# know exactly what the index is and so will return a less precise type:
int_index = 1
reveal_type(tup[int_index]) # Revealed type is 'Union[str, float]'
# But if we use either Literal types or a Final int, we can gain back
# the precision we originally had:
lit_index: Literal[1] = 1
fin_index: Final = 1
reveal_type(tup[lit_index]) # Revealed type is 'str'
reveal_type(tup[fin_index]) # Revealed type is 'str'
# We can do the same thing with with TypedDict and str keys:
class MyDict(TypedDict):
name: str
main_id: int
backup_id: int
d: MyDict = {"name": "Saanvi", "main_id": 111, "backup_id": 222}
name_key: Final = "name"
reveal_type(d[name_key]) # Revealed type is 'str'
# You can also index using unions of literals
id_key: Literal["main_id", "backup_id"]
reveal_type(d[id_key]) # Revealed type is 'int'
.. _tagged_unions:

Tagged unions
*************

When you have a union of types, you can normally discriminate between each type
in the union by using ``isinstance`` checks. For example, if you had a variable ``x`` of
type ``Union[int, str]``, you could write some code that runs only if ``x`` is an int
by doing ``if isinstance(x, int): ...``.

However, it is not always possible or convenient to do this. For example, it is not
possible to use ``isinstance`` to distinguish between two different TypedDicts since
at runtime, your variable will simply be just a dict.

Instead, what you can do is *label* or *tag* your TypedDicts with a distinct Literal
type. Then, you can discriminate between each kind of TypedDict by checking the label:

.. code-block:: python
from typing import Literal, TypedDict, Union
class NewJobEvent(TypedDict):
tag: Literal["new-job"]
job_name: str
config_file_path: str
class CancelJobEvent(TypedDict):
tag: Literal["cancel-job"]
job_id: int
Event = Union[NewJobEvent, CancelJobEvent]
def process_event(event: Event) -> None:
# Since we made sure both TypedDicts have a key named 'tag', it's
# safe to do 'event["tag"]'. This expression normally has the type
# Literal["new-job", "cancel-job"], but the check below will narrow
# the type to either Literal["new-job"] or Literal["cancel-job"].
#
# This in turns narrows the type of 'event' to either NewJobEvent
# or CancelJobEvent.
if event["tag"] == "new-job":
print(event["job_name"])
else:
print(event["job_id"])
While this feature is mostly useful when working with TypedDicts, you can also
use the same technique wih regular objects, tuples, or namedtuples.

Similarly, tags do not need to be specifically str Literals: they can be any type
you can normally narrow within ``if`` statements and the like. For example, you
could have your tags be int or Enum Literals or even regular classes you narrow
using ``isinstance()``:

.. code-block:: python
from typing import Generic, TypeVar, Union
T = TypeVar('T')
class Wrapper(Generic[T]):
def __init__(self, inner: T) -> None:
self.inner = inner
def process(w: Union[Wrapper[int], Wrapper[str]]) -> None:
# Doing `if isinstance(w, Wrapper[int])` does not work: isinstance requires
# that the second argument always be an *erased* type, with no generics.
# This is because generics are a typing-only concept and do not exist at
# runtime in a way `isinstance` can always check.
#
# However, we can side-step this by checking the type of `w.inner` to
# narrow `w` itself:
if isinstance(w.inner, int):
reveal_type(w) # Revealed type is 'Wrapper[int]'
else:
reveal_type(w) # Revealed type is 'Wrapper[str]'
This feature is sometimes called "sum types" or "discriminated union types"
in other programming languages.

Limitations
***********

Expand Down
13 changes: 13 additions & 0 deletions docs/source/more_types.rst
Original file line number Diff line number Diff line change
Expand Up @@ -1119,3 +1119,16 @@ and non-required keys, such as ``Movie`` above, will only be compatible with
another ``TypedDict`` if all required keys in the other ``TypedDict`` are required keys in the
first ``TypedDict``, and all non-required keys of the other ``TypedDict`` are also non-required keys
in the first ``TypedDict``.

Unions of TypedDicts
--------------------

Since TypedDicts are really just regular dicts at runtime, it is not possible to
use ``isinstance`` checks to distinguish between different variants of a Union of
TypedDict in the same way you can with regular objects.

Instead, you can use the :ref:`tagged union pattern <tagged_unions>`. The referenced
section of the docs has a full description with an example, but in short, you will
need to give each TypedDict the same key where each value has a unique
unique :ref:`Literal type <literal_types>`. Then, check that key to distinguish
between your TypedDicts.

0 comments on commit 7055725

Please sign in to comment.