Skip to main content

Literals and overloading in Python

It’s time to talk about Pythons Literals and I mean that literally :smile:.

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

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

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

We get a warning: Type warning

What about this:

b = fun("all")
b  + "all"

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>

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

No warnings :)

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

b = fun("all")
c = b + 1

Image description

References

comments powered by Disqus