Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Update docs for Literal types #8152

Merged
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Jump to
Jump to file
Failed to load files.
Diff view
Diff view
161 changes: 141 additions & 20 deletions docs/source/literal_types.rst
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'
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Maybe add an example with TypedDict? Maybe also an example with union of literals, something along these lines:

i: Literal[1, 2]
t: Tuple[str, int, int]
reveal_type(t[i])  # This is "int"


# 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.
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Mention that tags can also be proper enum values (using is) and classes (using isinstance()), not just strings.


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
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.