Function overloading - singledispatch in Python with type hint

Using singledispatch with type hint
python
Published

November 16, 2022

With Python>=3.7, the @singledispatch method can now understand the type hints. It behaves like function overloading but it’s more dynamic than the static langauge.

Here is a quick example to demonstrate it.

from functools import singledispatch

@singledispatch
def foo(x):
    print("foo")
@foo.register
def _(x: float):
    print("It's a float")


@foo.register
def _(x: str):
    print("It's a string now!")

Let’s see how it works.

foo(1)
foo
foo(1.0)
It's a float
foo("1")
It's a string now!

The function foo now understand the type of the argument and dispatch the corresponding functions. This is nicer than a big chunk of if/else statement since it’s less couple. It’s also easy to extend this. Imagine the foo function is import from a package, it’s easy to extend it.

# Imagine `foo` was imported from a package
# Now that you have a special type and you want to extend it from your own library, you don't need to touch the source code at all.

# from bar import foo
class Nok:
    ...


@foo.register
def _(x: Nok):
    print("Nok")


nok = Nok()
foo(nok)
Nok

This is only possible because Python is a dynamic language. In contrast, to achieve the same functionalities with monkey patching, you would need to copy the source code of the function and extend the if/else block.

Let’s dive a bit deeper to the decorator.

print([attr for attr in dir(foo) if not attr.startswith("_")])
['dispatch', 'register', 'registry']
foo.dispatch
<function functools.singledispatch.<locals>.dispatch(cls)>
foo.register
<function functools.singledispatch.<locals>.register(cls, func=None)>
foo.registry
mappingproxy({object: <function __main__.foo(x)>,
              float: <function __main__._(x: float)>,
              str: <function __main__._(x: str)>,
              __main__.Nok: <function __main__._(x: __main__.Nok)>,
              __main__.Nok: <function __main__._(x: __main__.Nok)>})
from collections import abc
isinstance(foo.registry, abc.Mapping)

The foo.registry is the most interesting part. Basically, it’s a dictionary-like object which store the types. It behaves like

if type(x) == "int":
    do_something()
elif type(x) == "float":
    do_somthing_else()
else:
    do_this_instead()