Python Type Hints - How to Use typing.Literal

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.
Por Exemplo
To use 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 "checkers"
and "chess"
.
Allowed Types
typing.Literal
was defined in PEP 586, which defined the allowed types for values:
int
sstr
sbytes
esbool
senum.Enum
valuesNone
- a special case for convenience:Literal[None]
is equivalent toNone
Additionally, we can nest Literal
s, which combines their values. For example Literal[Literal[1, 2], Literal[3], 4]
is equivalent to Literal[1, 2, 3, 4]
.
Benefits
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.
- Chained
if/elif
statements use all permitted values, when we use exhaustiveness checking.
Let’s examine those in turn.
1. Assignments
Imagine we make a typo and assign an incorrect value to our game
variable:
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 Literal
:
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)
3. Comparisons
We might also use an unsupported value in a comparison against a Literal
value:
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 ==
and !=
. 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 if isinstance(...)
.
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 Literal
types:
$ 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 NoReturn
type. 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 Literal
types.
Imagine we forgot to handle the "chess"
case in our get_game_count()
function:
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 reports:
$ 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.
If your Django project’s long test runs bore you, I wrote a book that can help.
One summary email a week, no spam, I pinky promise.
Related posts: