DEV Community

Piotr
Piotr

Posted on • Edited on

Literals and overloading in Python

It's time to talk about Pythons Literals and I mean that literally 😄.

Kevin from the office laughing

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 return pandas.DataFrame or pandas.Series
  • pandas.to_datetime which can return datetime.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
Enter fullscreen mode Exit fullscreen mode

Type checker will know that a should always be int 5 and will show a warning if we try to change that:

Pylint warning

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 
Enter fullscreen mode Exit fullscreen mode

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
Enter fullscreen mode Exit fullscreen mode

We get a warning: Type warning

What about this:

b = fun("all")
b  + "all"
Enter fullscreen mode Exit fullscreen mode

Another type warning

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>
Enter fullscreen mode Exit fullscreen mode

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
Enter fullscreen mode Exit fullscreen mode

As you can see, this function grew, but we are now able to do this like this:

b = fun("number")
c = b + 1
Enter fullscreen mode Exit fullscreen mode

No warnings :)

without any warnings 😎. And be warned if the return type changes:

b = fun("all")
c = b + 1
Enter fullscreen mode Exit fullscreen mode

Image description

References

Top comments (2)

Collapse
 
aktentasche profile image
Jonas Manthey

Why not use an Union for the return type

Collapse
 
finloop profile image
Piotr

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.