Python Type Hints - How to Use typing.Literal2021-07-09
To put it tautologically, type hints normally specify the types of variables.
But when a variable can only contain a limited set of literal values, we can use
typing.Literal for its type.
This allows the type checker to make extra inferences, giving our code an increased level of safety.
In this post we’ll look at how to use
Literal and the benefits it provides.
Literal, we simply parametrize it with the allowed literal values:
from typing import Literal game: Literal["checkers", "chess"]
We’ve declared that the
game variable has only two possible values: the strings
typing.Literal was defined in PEP 586, which defined the allowed types for values:
None- a special case for convenience:
Literal[None]is equivalent to
Additionally, we can nest
Literals, which combines their values.
Literal[Literal[1, 2], Literal, 4] is equivalent to
Literal[1, 2, 3, 4].
When we use a
Literal type, the type checker can ensure that:
- Assignments use permitted values.
- Function calls use permitted values.
- Comparisons use permitted values.
- Conditional blocks use the subset of values they compared against, via specialized type narrowing.
if/elifstatements use all permitted values, when we use exhaustiveness checking.
Let’s examine those in turn.
Imagine we make a typo and assign an incorrect value to our
from typing import Literal game: Literal["checkers", "chess"] game = "chuss"
Mypy will spot this for us:
$ mypy example.py example.py:5: error: Incompatible types in assignment (expression has type "Literal['chuss']", variable has type "Union[Literal['checkers'], Literal['chess']]") Found 1 error in 1 file (checked 1 source file)
2. Function calls¶
Similarly, say we pass an unsupported value to a function using
from typing import Literal def get_game_count(game: Literal["checkers", "chess"]) -> int: ... get_game_count("chockers")
Mypy will also find this:
$ mypy example.py example.py:8: error: Argument 1 to "get_game_count" has incompatible type "Literal['chuss']"; expected "Union[Literal['checkers'], Literal['chess']]" Found 1 error in 1 file (checked 1 source file)
We might also use an unsupported value in a comparison against a
from typing import Literal game: Literal["checkers", "chess"] = "checkers" if game == "owela": ...
In this case, as long as we have Mypy’s
strict_equality option active, it will find the error:
$ mypy --strict-equality example.py example.py:5: error: Non-overlapping equality check (left operand type: "Union[Literal['checkers'], Literal['chess']]", right operand type: "Literal['owela']") Found 1 error in 1 file (checked 1 source file)
Note that, at time of writing, Mypy only performs strict equality checks for
A more complicated comparison, like
game in ["owela"], will not fail the strict equality check.
4. Conditional block type narrowing¶
When we make a comparison against a variable’s type, Mypy can perform type narrowing to infer the variable has a restricted type within the conditional block.
We previously covered how to do this with constructs like
When we compare a
Literal against one or more values, Mypy will also perform type narrowing.
It can infer the value has a more limited
Literal type within the conditional block.
For example, take this code:
from typing import Literal game: Literal["checkers", "chess"] = "checkers" if game == "checkers": reveal_type(game) else: reveal_type(game)
When we run Mypy on it, the
reveal_type() debug calls show us the narrowed
$ mypy --strict-equality example.py example.py:6: note: Revealed type is "Literal['checkers']" example.py:8: note: Revealed type is "Literal['chess']"
5. Exhaustiveness Checking¶
Exhaustiveness checking is when the type checker ensures we cover all possible options for a variable’s type or value.
Python type hints have no formal specification for exhaustiveness checking yet, but we can emulate it with the
If we use a function that accepts
NoReturn as a value type, any call to it will fail, since
NoReturn matches no type.
This technique was documented for
Enum types in a blog post by Haki Benita.
We can also use it with
Imagine we forgot to handle the
"chess" case in our
from typing import Literal, NoReturn def assert_never(value: NoReturn) -> NoReturn: """Exhaustiveness checking failure function""" assert False GameType = Literal["checkers", "chess"] def get_game_count(game: GameType) -> int: if game == "checkers": count = 123 else: assert_never(game) return count
$ mypy example.py example.py:16: error: Argument 1 to "assert_never" has incompatible type "Literal['chess']"; expected "NoReturn" Found 1 error in 1 file (checked 1 source file)
The error message is not particularly clear, as we’re only emulating exhaustiveness checking.
But it does report the unhandled value is
Literal['chess'], and the approximate line to correct that on.
Exhaustiveness checking is particularly useful when we introduce a new value into our system.
We can add the value to our first shared
Literal definition, and then use Mypy to find places that need updating.
I hope that you’ve literally enjoyed this post,
🎉 My book Speed Up Your Django Tests is now up to date for Django 3.2. 🎉
Buy now on Gumroad
One summary email a week, no spam, I pinky promise.
Tags: mypy, python
© 2021 All rights reserved.