It's time to talk about Pythons Literals and I mean that literally π.
Now that we got that unfunny joke out of the way.
What are Literals and why are they usefull
The basic motivation behind them is that functions can have arguments that can only take a specific set of values, and those functions return values/types change based on that input. Common examples are (you can find more here):
-
pandas.concat
which can returnpandas.DataFrame
orpandas.Series
-
pandas.to_datetime
which can returndatetime.datetime
,DatetimeIndex
,Series
,DataFrame
...
It would be a problem If we couldn't know what type the return value is. Literals can help us indicate that an expression has only a specific value. If we combine that with overloading we can add type hints to those type of functions. But before I'll get to examples that change their return types, let's start with something simple:
from typing import Literal
a: Literal[5] = 5
Type checker will know that a
should always be int 5 and will show a warning if we try to change that:
More examples
Let's define a function whose return type change depending on the input value. But let's do that without literals and overloading:
def fun(param):
if param == "all":
return "all"
elif param == "number":
return 1
This function takes an argument param
and returns all
or number 1. Return type of this function is Literal["all", 1]
, but if we try to do this:
b = fun("number")
b + 1
What about this:
b = fun("all")
b + "all"
Type checker doesn't know what is the return type of that function is. We can help him with that by doing an overload.
Overloading
Overloading in python allows describing functions that have multiple combinations of input and output types (but only one definition). You can overload a function using an overload
decorator like this:
from typing import overload
@overload
def f(a: int) -> int:
...
@overload
def f(a: str) -> str:
...
def f(a):
<implementation of f>
Create a function first and above it. Then add a series of functions with @overload
decorators, which will be used to help with guessing return types.
Now back to Literals. How to fix function fun
? Easy - overload it (and add type hints, just to make sure).
@overload
def fun(param: Literal["all"]) -> Literal["all"]:
...
@overload
def fun(param: Literal["number"]) -> int:
...
def fun(param: Literal["all", "number"]) -> Literal["all"] | int:
if param == "all":
return "all"
elif param == "number":
return 1
As you can see, this function grew, but we are now able to do this like this:
b = fun("number")
c = b + 1
without any warnings π. And be warned if the return type changes:
b = fun("all")
c = b + 1
Top comments (2)
Why not use an Union for the return type
In the first example Union is the return type. As Literal["all", 1] is union of Literal[all] and Literal[1]. But the warning about types still persists.
My point is that with overloading and Literals/types you can precisely specify return types of functions. Something that Unions cannot do.