Skip to content

Commit

Permalink
Update docs for Literal types
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 brief example of one).

3. Adds a section about "intelligent indexing".

4. Adds a section about the "tagged union" pattern (see
   python#8151). I made the example
   focus on the TypedDict/JSON use case -- IMO that's really the only
   realistically useful use case for the pattern.

5. Cross-references the "tagged union" docs with the TypedDicts docs --
   IMO, tagged unions are mostly useful when you're working with JSON

I also thought about making the "Unions of TypedDict" section I added
to the TypedDicts doc mention using [pydantic][0] as an alternative
to the "tagged union" pattern.

I personally prefer using libraries like this which handle validation
and let me use regular classes (and `isinstance`) instead of dicts, but I
wasn't sure whether we want to be recommending 3rd party libraries so
held off for now.

  [0]: https://pydantic-docs.helpmanual.io
  • Loading branch information
Michael0x2a committed Dec 16, 2019
1 parent a918ce8 commit 5798ba4
Show file tree
Hide file tree
Showing 2 changed files with 108 additions and 19 deletions.
114 changes: 95 additions & 19 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,9 @@ precise type signature for this function using ``Literal[...]`` and overloads:

.. code-block:: python
from typing import overload, Union
from typing_extensions import Literal
# Note: if you are using Python 3.7 or earlier, you will need to import
# Literal from the typing_extensions module.
from typing import overload, Union, Literal
# The first two overloads use Literal[...] so we can
# have precise return types:
Expand Down Expand Up @@ -56,15 +50,15 @@ precise type signature for this function using ``Literal[...]`` and overloads:
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 +82,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 +106,9 @@ you can instead change the variable to be ``Final`` (see :ref:`final_attrs`):

.. code-block:: python
from typing_extensions import Final, Literal
# Note: if you are using Python 3.7 or earlier, you will need to import
# Literal and Final from the typing_extensions module.
from typing import Final, Literal
def expects_literal(x: Literal[19]) -> None: pass
Expand All @@ -134,7 +127,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 Down Expand Up @@ -168,6 +161,89 @@ 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
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'
.. _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
# Note: if you are using Python 3.7 or earlier, you will need to import
# Literal and TypedDict from the typing_extensions module.
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: Union[NewJobEvent, CancelJobEvent]) -> 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.

This language 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 5798ba4

Please sign in to comment.