Python Type Hinting with Literal
PYTHON PROGRAMMING

I'll admit it: I wasn't always a fan of typing.Literal
, a form of creating literal types in Python. In fact, I not only undervalued literal types, but I completely ignored them, refusing to use them at all. For some reason, which remains unclear to me even today, I couldn't find much practical value in literal types.
How wrong I was. I was blind to the power of this simple tool, and my code suffered as a result. If you've been ignoring literal types like I did, I urge you to read this article. I hope to convince you that despite its simplicity, typing.Literal
can be a very useful tool in your Python coding arsenal.
Even if you've already recognized the value of literal types, don't stop reading. While we won't delve into all the intricacies of typing.Literal
, this article will provide a more comprehensive introduction than the official Python documentation, without getting as bogged down in details as PEP 586.
Literal types are so straightforward that they can make code clearer and more readable than code without them. This simplicity is their both a strength and a weakness of typing.Literal
, as it doesn't offer any additional functionalities. However, I'll show you how to implement additional functionality yourself.
The goal of this article is to introduce typing.Literal
and discuss its value in Python coding. Along the way, we'll explore when to use typing.Literal
– and, just as importantly, when not to.
Literal types
Literal types were introduced to the Python typing system by PEP 586. This PEP provides a comprehensive exploration of the proposal behind literal types, serving as a rich source of information on the subject. In contrast, the official documentation for the typing.Literal
type is intentionally concise, reflecting its straightforward nature. This article bridges the gap between these two resources, providing fundamental information about literal types while also delving into details that I consider crucial for the use cases discussed.
As explained in PEP 586, literal types are particularly useful in scenarios where APIs return different types based on the value of an argument. I would broaden this statement by saying that literal types allow for the creation of a type that encompasses specific values, not necessarily all of the same type. This does not preclude the possibility of all values having the same type.
Literal types provide a remarkably simple approach to defining and utilizing a type with specific values as the only possible values. This simplicity far surpasses any alternative methods. While it's true that you can achieve the same outcome using other methods, these alternatives often come with more complex implementations and potentially richer functionality. For instance, creating your own type (class) requires careful consideration of both design and implementation – something you can ignore altogether when creating a literal type instead.
Employing typing.Literal
invariably presents a simpler solution, often significantly simpler, but at the expense of reduced functionality. Therefore, before making a decision, it's essential to carefully weigh the advantages and disadvantages of both approaches. This article can assist you in making an informed choice.
Acceptable types in literals
To create a typing.Literal
type, you can use the following values:
- a literal value of
int
,bool
,str
orbytes
- an enum value
None
Such types as float
or instances of a custom (non-enum) class are unacceptable.
Literal types: Use cases
We'll now explore several use cases where I consider literal types to be an excellent choice, often the best option. We'll also examine situations where alternative solutions may be more suitable. Each use case assumes the need for a type that accepts only specific values, not necessarily of the same type. typing.Literal
does not create empty types, so Literal[]
is not valid. It can, however, create literal types with a single value.
The use cases discussed below do not constitute an exhaustive list of scenarios. Instead, they serve as examples, and some may overlap. This non-exclusive list aims to showcase the range of opportunities that typing.Literal
offers and to enhance understanding of this intriguing and valuable tool.
Example 1: One value only
As previously mentioned, you can employ a literal type when a variable accepts only a single value. While this might seem counterintuitive at first glance, the typing.Literal
documentation provides a relevant example:
def validate_simple(data: Any) -> Literal[True]:
...
This function is designed for data validation and always returns True
. In other words, if the validation fails, the function raises an error; otherwise, it returns True
.
Theoretically, a type signature with a return value of the bool
type, as shown below, would be acceptable to static checkers:
def validate_simple(data: Any) -> bool:
...
However, the function never returns False
, making this type hint misleading and inaccurate. Using bool
implies that the function can, depending on the situation, return either of the two Boolean values. When a function consistently returns only one of these values and never the other, using bool
is misleading.
This is precisely where a literal type comes into play. Not only does it satisfy static checkers, but it also provides valuable information to users.
Example 2: In a need of a static type
When runtime type checking is not required, static types often provide the most effective solution. Therefore, if you need a type that accepts one or more specific values and your primary goal is to inform static checkers, creating the corresponding literal type is an excellent approach.
Example 3: A number of strings
This use case encompasses a range of strings, such as modes, products, or colors. Here are some examples:
Colors = Literal["white", "black", "grey"]
Grey = Literal["grey", "gray", "shades of grey", "shades of gray"]
Mode = Literal["read", "write", "append"]
As you can see, literal types in this use case can hold two or more strings. Importantly, using Literal
does not allow us to establish relationships between the individual values. For instance, we could create the following literal type:
Days = Literal[
"Monday", "Tuesday", "Wednesday",
"Thursday", "Friday", "Saturday", "Sunday"
]
Does the order in which the values are provided matters? Before Python 3.9.1, it did:

but ever since it doesn't:

Consequently, what matters are the possible choices, not their relationships. If utilizing the order of values is essential, consider employing a different type, not a literal one. One solution is to leverage an enumeration type, utilizing the enum.Enum
class; we'll delve into this concept soon, in a dedicated article.
A word of caution: Python 3.11 and newer introduce typing.LiteralString
. This constitutes a distinct tool, as unlike typing.Literal
, it serves as a type itself, not a tool for creating types. In this article, we're exploring the creation of literal types, and I wouldn't want to introduce confusion with this slightly different yet related tool. If you're interested in learning more, visit the Appendix at the end of the article. However, let's set this topic aside for now. The key takeaway is that typing.LiteralString
is not a substitute for typing.Literal
for strings.
typing.LiteralString
is not a replacement fortyping.Literal
for strings.
Example 4: Multiple values of the same type
This example extends the previous one to encompass a broader range of data types. Just as we employed literal types for strings, we can apply them to most other data types as well. Here are some examples:
Literal[1, 5, 22] # integers
Literal["1", "5", "22"] # strings
As mentioned above, you can use a literal value of int
, bool
, str
or bytes
, an enum value and None.
Example 5: Combining values of various types
This represents the most general form of a literal type. You can combine objects of any type, and it will function correctly. This bears some resemblance to using the typing.Union
type, but unlike the typical Union
use case, we are combining objects rather than types.
Note the difference: A common Union use case might look like this:
Union[int, str]
while a literal type combining objects of int
and str
types could be as follows:
Tens = Literal[10, "10", "ten"]
Here are some other examples:
Positives = Literal[True, 1, "true", "yes"]
Negatives = Literal[False, 0, "false", "no"]
YesOrNo = Literal[Positives, Negatives]
You can create the following type: Literal[True, False, None]
. It's similar to the OptionalBool
type described here:
The OptionalBool
type described in the above article is far more complex than the corresponding one based on Literal
, the latter being both easier to use and understand but also having significantly poorer functionality.
The next three examples from the code block above are also interesting. They show that you can create combinations of two (or more, for that matter) literal types. Here, YesOrNo
is a literal type that joins two other literal types, that is, Positives
and Negatives
:

Do remember, however, that this wouldn't work the same way before Python 3.9.1 (we saw it before, where we discussed the order of literals in type definition):

Example 6: Runtime membership checking
In the preceding examples, we focused exclusively on static applications of literal types. However, this does not preclude their use during runtime, even though this deviates from the intended purpose of Python type hints. Here, I'll demonstrate that you can perform runtime membership checks for literal types when the need arises. In other words, you can verify whether a given value belongs to the set of possible choices for a literal type.
Frankly, I believe this single capability elevates typing.Literal
to a much more powerful tool. While it strays from the conventional usage of literal types (static code checking), it isn't a hack. It's a legitimate function of the typing module: typing.get_args()
.
An example will best illustrate this concept. First, let's define a literal type:
from typing import Any, get_args, Literal, Optional
Tens = Literal[10, "10", "ten"]
The Tens
type encompasses various representations of the number 10
. Now, let's define a function that validates whether an object has the type of Tens
:
def is_ten(obj: Any) -> Optional[Tens]:
if obj in get_args(Tens):
return obj
return None
A few remarks about the function:
- It accepts any object and returns
Optional[Tens]
, indicating that ifobj
is a valid member ofTens
, the function will return it; otherwise, it will returnNone
. This is whytyping.Optional
is used (see this article). - The check is performed using the
typing.get_args()
function. For a literal type, it returns all its possible values. - Here's where it gets interesting. From a dynamic perspective, the last line of the function (
return None
) is redundant, as an absentNone
return is implicitly interpreted as aNone
return. However,mypy
does not accept implicit None returns, as illustrated in the image below.

According to the mypy
documentation, you can disable strict None
checking using the [[--no-strict-optional](https://mypy.readthedocs.io/en/stable/command_line.html#cmdoption-mypy-no-strict-optional)](https://mypy.readthedocs.io/en/stable/command_line.html#cmdoption-mypy-no-strict-optional)
command-line option. Think twice if you want to use this option. I prefer to always explicitly declare whether a particular type accepts None
or not. Disabling strict checking means that any type is assumed to accept None
, which can lead to unexpected behavior and make code more difficult to understand and maintain. While I am not a great fan of very thorough type hints, using the --no-strict-optional
flag is in my eyes an oversimplification, because None
is too important a sentinel value to ignore it just like that.
If you do need to disable strict checking in specific situations, remember that when you do so but someone else doesn't, they may encounter many static errors throughout the code. Maintaining consistent type checking settings throughout a codebase is a good general practice.
Literals versus enumerations
While reading the previous section, did you notice that some literal types resemble enumerations types? Indeed, they do share some similarities, but literal types lack the natural order of values inherent in enumerations.
Compare these two type definitions:
from typing import Literal
from enum import Enum
ColorsL = Literal["white", "black", "grey"]
class ColorsE(Enum):
WHITE = "white"
BLACK = "black"
GREY = "grey"
If you primarily noticed the difference in syntax, be aware that you can also define enumeration types using static factory methods:
ColorsE2 = Enum("ColorsE2", ["WHITE", "BLACK", "GREY"])
ColorsE3 = Enum("ColorsE3", "WHITE BLACK GREY")
So, the definition syntax isn't the key distinction between literal types and enumerations. Firstly, literal types are static types with minor dynamic functionality, while enumeration types offer both static and dynamic capabilities, making them more versatile. If you require more than what literal types provide, enumerations are likely the better choice.
This article doesn't delve into the intricacies of Python enumerations. However, the following table compares the two tools. Before proceeding, analyze the table and observe that typing.literal
offers a subset of enum.Enum
‘s features.

Despite their versatility, literal types excel in simplicity, brevity, and readability. While Python enumerations are also straightforward and readable, literal types offer an even higher level of clarity and conciseness.
Conclusion
The central message of this article is that typing.Literal
and literal types are powerful tools that offer more capabilities than one might initially assume. Their simplicity conceals their depth and versatility. As I mentioned at the beginning of the article, I had underestimated the value of this tool for quite some time. However, today I recognize it – and literal types in general – as a powerful yet straightforward mechanism for enhancing Python code conciseness while maintaining static correctness.
In fact, using other type hints to express the same concept as a literal type can lead to confusion, even if static checkers don't raise any errors. When all you need is a static type to be checked by static checkers, typing.Literal
should be your go-to choice. Its usage is straightforward and doesn't require excessive code: just the type definition, which typically takes one or more lines depending on the number of literals included in the type.
For scenarios requiring more advanced dynamic functionality, enumerations may be a better fit. They provide an additional layer of safety at runtime by preventing invalid value assignments. Literal types, on the other hand, do not offer this inherent safeguard, although it can be implemented as demonstrated with the is_ten()
function above. However, this safeguard would need to be applied every time a user provides a value of this type.
In essence, remember about literal types and typing.Literal
. Incorporate them into your Python code to achieve simplicity and readability. I'd say that in Python, typing.Literal
achieves one of the highest usefulness-to-complexity ratios, making it simultaneously highly useful and remarkably simple.
Appendix 1
typing.LiteralString
Python 3.11 and newer introduced the typing.LiteralString
type. Despite its name, it is not a direct replacement for typing.Literal
for strings. To avoid unnecessary confusion, let's not delve into this type in detail here. Instead, let's briefly outline the fundamental aspects of this type.
Unlike typing.Literal
, which serves as a mechanism for creating literal types, typing.LiteralString
is a type itself. It can be used to specify that a variable should hold a literal string, as demonstrated in the following example:
from typing import LiteralString
def foo(s: LiteralString) -> None
...
Note what the documentation says:
Any string literal is compatible with
LiteralString
, as is anotherLiteralString
. However, an object typed as juststr
is not.
And
LiteralString
is useful for sensitive APIs where arbitrary user-generated strings could generate problems. For example, the two cases above that generate type checker errors could be vulnerable to an SQL injection attack.
This brief overview should suffice for our current discussion. If you're interested in exploring this type further, refer to PEP 675, which introduced this literal type.
Appendix 2
Defining literal types using iterables
Warning: This section presents a hack that does not work statically. So, if your only aim is to create a static type, do not use this hack. It's rather an interesting piece of information than something to be used in production code.
If you are not familiar with typing.Literal
, Literal[]
might resemble indexing, and Literal[1, 2, 3]
might appear similar to a list. As a result, you might be tempted to use a list comprehension, as shown here:
>>> OneToTen = Literal[i for i in range(1, 11)]
File "", line 1
OneToTen = Literal[i for i in range(1, 11)]
^^^
SyntaxError: invalid syntax
The error message indicates that this is not valid syntax. This is because typing.Literal
is not meant to be used as a list comprehension. Instead, it is used to specify particular values the type accepts.
But look here:
>>> OneToTen = Literal[[i for i in range(1, 11)]]
No error? So, we're fine, aren't we?
No, we aren't. Look at what OneToTen
is:
>>> OneToTen
typing.Literal[[1, 2, 3, 4, 5, 6, 7, 8, 9, 10]]
>>> get_args(OneToTen)
([1, 2, 3, 4, 5, 6, 7, 8, 9, 10],)
As you can see, this definition worked but not in the way we intended. OneToTen
is a literal type with only one value: a list of integers from 1 to 10. Not only is a list not an acceptable literal type, this is also not quite what we were hoping for!
But don't worry, we're not done here. There's a trick that will help us achieve the desired outcome. We can access the possible values of a literal type in two ways. One method, which we've already seen in action, is the get_args()
function. Another method is to use the .__args__
attributeof the type:
>>> get_args(OneToTen)
([1, 2, 3, 4, 5, 6, 7, 8, 9, 10],)
>>> OneToTen.__args__
([1, 2, 3, 4, 5, 6, 7, 8, 9, 10],)
>>> get_args(OneToTen) == OneToTen.__args__
True
While get_args()
allows us to get a literal type's values, we can leverage the .__args__
attribute to update the type. Look:
>>> OneToTen.__args__ = [1, 2, 3, 4, 5, 6, 7, 8, 9, 10]
>>> OneToTen
typing.Literal[1, 2, 3, 4, 5, 6, 7, 8, 9, 10]
Ha! This is the trick I mentioned above. We can call it the .__args__
trick.
Above, I used a list, but it doesn't matter what type of iterable you'll use:
>>> OneToTen == Literal[1, 2, 3, 4, 5, 6, 7, 8, 9, 10]
True
>>> OneToTen.__args__ = (1, 2, 3, 4, 5, 6, 7, 8, 9, 10)
>>> OneToTen == Literal[1, 2, 3, 4, 5, 6, 7, 8, 9, 10]
True
>>> OneToTen.__args__ = {1, 2, 3, 4, 5, 6, 7, 8, 9, 10}
>>> OneToTen == Literal[1, 2, 3, 4, 5, 6, 7, 8, 9, 10]
True
I assigned a list literal to OneToTen.__args__
, but you can do the same in any other way, like using a list comprehension or another comprehension:
>>> OneToTen.__args__ = [i for i in range(1, 11)]
>>> OneToTen == Literal[1, 2, 3, 4, 5, 6, 7, 8, 9, 10]
True
>>> OneToTen.__args__ = list(range(1, 11))
>>> OneToTen == Literal[1, 2, 3, 4, 5, 6, 7, 8, 9, 10]
True
>>> OneToTen.__args__ = {i for i in range(1, 11)}
>>> OneToTen == Literal[1, 2, 3, 4, 5, 6, 7, 8, 9, 10]
True
You do have to be careful, however, as not always will Literal
behave in a predictable way. For instance, it will work like above with range()
but won't work with a generator expression:
>>> OneToTen.__args__ = range(1, 11)
>>> OneToTen == Literal[1, 2, 3, 4, 5, 6, 7, 8, 9, 10]
True
>>> OneToTen.__args__ = (i for i in range(1, 11))
>>> OneToTen == Literal[1, 2, 3, 4, 5, 6, 7, 8, 9, 10]
False
>>> OneToTen.__args__
at 0x7f...>
Actually, while experimenting with generator expressions used with Literal
, I noticed that it did work several times… I don't know why: normally it doesn't work that way, so out of say two dozen times I tried it, it worked only 2 or 3 times. That's something I'm worried about as I hate situations in which a Programming language behaves in an unpredictable way – even if in a hack.
Having troubles believing this? Look at this screenshot from Python 3.11:

Just so you know, A
was not used before, but OneToTen
was – on the other hand, this should not change a thing. Besides, the next time I tried this, this time for a new name, B
, it didn't work:

Hence, unless you're ready to accept unpredictable behavior of Python, don't use typing.Literal
with generator expressions before this issue is solved. But there's nothing to worry about, as generator expressions are typically used to overcome memory issues – and creating a literal type doesn't seem like something that should lead to such problems. Hence, instead of using a generator to create a literal type, you can make a list out of it and use it.
As mentioned at the beginning of this section, you should avoid using the .__args__
hack. It will work dynamically, but mypy
will not accept it. It's good to know this, as it extends your knowledge of typing
type hints, but it's not something you should use in production code.
Thanks for reading. If you enjoyed this article, you may also enjoy other articles I wrote; you will see them here. And if you want to join Medium, please use my referral link below: