Overloading Functions with Python Singledispatch

Photo by Maddi Bazzocco on Unsplash

Sometimes, you may need to implement a slight different variation of one function call depends on the type of the passed in parameter. Well you could have a long and tedious switch or if..else... statement together with isinstance or type . But there is a helper function to solve your parametric polymorphism problem.

In programming languages and type theory, parametric polymorphism is a way to make a language more expressive, while still maintaining full static type-safety. Using parametric polymorphism, a function or a data type can be written generically so that it can handle values identically without depending on their type. Such functions and data types are called generic functions and generic datatypes respectively and form the basis of generic programming.

For example, a function append that joins two lists can be constructed so that it does not care about the type of elements: it can append lists of integers, lists of real numbers, lists of strings, and so on. Let the type variable a denote the type of elements in the lists. Then append can be typed

forall a. [a] × [a] -> [a]

Following Christopher Strachey, parametric polymorphism may be contrasted with ad hoc polymorphism, in which a single polymorphic function can have a number of distinct and potentially heterogeneous implementations depending on the type of argument(s) to which it is applied. Thus, ad hoc polymorphism can generally only support a limited number of such distinct types, since a separate implementation has to be provided for each type.

— wiki

Starting Python 3.4. , there is a decorator in functools called singledispatch been added. In Python 3.8, there is another decorator called singledispatchmethod.been added.

Essentially, what these decorators do is to help overload a function which is essentially a function with different implementations. Calling an overloaded function will invoke one implementation of the many, which is a generic function, based on some prior conditions.

A generic function is composed of multiple functions implementing the same operation for different types. Which implementation should be used during a call is determined by the dispatch algorithm. When the implementation is chosen based on the type of a single argument, this is known as single dispatch.


Still too abstract? Let’s look at an example.

from functools import singledispatch
from decimal import Decimal
When there is no registered implementation found, its MRO is used to find a more generic implementation. Hence the original function decorated is registered for the base object type, and is used if no other implementation is found.
def calc_num(num):
raise NotImplementedError("cannot calculate for unknown number type")
def calc_int(num):
print(f"int: {num}")
def calc_float(num):
print(f"float: {num}")
The decorator also supports decorator stacking, so we can create an overloaded function to handle multiple types.
def calc_float_or_decimal(num):
print(f"float/decimal: {round(num, 2)}")
===================int: 1
float/decimal: 1.0
float/decimal: 1.02
NotImplementedError: cannot calculate for unknown number type

You can also overload it with the customised function.

from functools import singledispatch
from dataclasses import dataclass
class Tea:
kind: str
temp: int
class Coffee:
kind: str
temp: int
def boil(obj=None):
raise NotImplementedError("No boiler instruction for this drink")
def _coffee_boil(obj):
return "Successfully boiled coffee!"
def _tea_boil(obj):
return "Successfully boiled tea!"

tea = Tea(kind="white tea", temp=93)
coffee = Coffee(kind="Yunnan", temp=98)
Successfully boiled tea!
Successfully boiled coffee!

Since the singledispatch can only dispatch based on the first argument passed, this becomes a problem with class method, as the default first self argument will take the spot. In this case, we can use the functools.singledispatchmethod, which transform a method into a single-dispatch generic function.

To define a generic method, decorate it with the @singledispatchmethod decorator. Note that the dispatch happens on the type of the first non-self or non-cls argument, create your function accordingly:

class Negator:
def neg(self, arg):
raise NotImplementedError("Cannot negate a")
def _(self, arg: int):
return -arg
def _(self, arg: bool):
return not arg

To check which implementation will the generic function choose for a given type, use the dispatch() attribute:

>>> fun.dispatch(float)
<function fun_num at 0x1035a2840>
>>> fun.dispatch(dict) # note: default implementation
<function fun at 0x103fe0000>

To access all registered implementations, use the read-only registry attribute:

>>> fun.registry.keys()
dict_keys([<class 'NoneType'>, <class 'int'>, <class 'object'>,
<class 'decimal.Decimal'>, <class 'list'>,
<class 'float'>])
>>> fun.registry[float]
<function fun_num at 0x1035a2840>
>>> fun.registry[object]
<function fun at 0x103fe0000>

That’s so much of it!

Happy Reading!




Hi :)

Love podcasts or audiobooks? Learn on the go with our new app.

Recommended from Medium

Python Basics: Loops and Conditionals

Are you looking forward to being a software engineer?

How To Fix Asus PadFone S Not Charging [Troubleshooting Guide]

How To Fix Asus Zenfone 7 Pro Not Charging [Troubleshooting Guide]

Final Writeup

Remote SSH and VNC with `remote.it` to access your devices remotely.

Java 9 — An uplifted documentation

Useful Tools for Software Devs

Get the Medium app

A button that says 'Download on the App Store', and if clicked it will lead you to the iOS App store
A button that says 'Get it on, Google Play', and if clicked it will lead you to the Google Play store


Hi :)

More from Medium

Factory Method Design Pattern in Python — Explained With Code

Caching function calls in Python 🐍

666 Python repos checked. 5409 open(…) calls. 11% were never closed!

Creating An Immutable Bag of Constants in Python