It's time to talk about Pythons Literals and I mean that literally 😄.
Now that we got that unfunny joke out of the way.
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.concatwhich can return
pandas.to_datetimewhich can return
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
Type checker will know that
a should always be int 5 and will show a warning if we try to change that:
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 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