<jupyter_start><jupyter_text>Preface* Overloading* Single Dispatch* Multiple Dispatch* Closing Thoughts([article source](https://martinheinz.dev/blog/50)) Function overloading is a common programming pattern which seems to be reserved to statically-typed, compiled languages. Yet there's an easy way to implement it in Python with help of Multiple Dispatch or as it's called in Python multimethods.  OverloadingFirst things first - you might be asking, how can we implement method overloading in Python when we all know that it's not possible? Well, even though Python is dynamically-typed language and therefore cannot have proper method overloading as that requires the language to be able to discriminate between types at compile-time, we can still implement it in a bit different way that is suitable for dynamically-typed languages.This approach is called Multiple Dispatch or multimethods, where the interpreter differentiates between multiple implementations of a function/method at runtime based on dynamically determined types. To be more precise, the language uses types of arguments passed to a function during its invocation to dynamically choose which one of the multiple function implementations to use (or dispatch).Now you might be thinking: "Do we really need this though? If it can't be implemented normally, maybe we shouldn't use it in Python..." Yea, valid point, but there are good reasons to want to implement some form of function/method overloading in Python. It's powerful tool that can make code more concise, readable and minimise its complexity. Without multimethods though, the "obvious way" to do this is using type inspection with isinstance(). This is very ugly, brittle solution that is closed to extension and I would call it an anti-pattern.Besides that, there already is method overloading in Python for operators and methods like [len()](https://docs.python.org/3/library/functions.htmllen) or new() using so-called dunder or magic methods (see docs [here](https://docs.python.org/3/reference/datamodel.htmlspecial-method-names)) and we all use that quite often, so why not use proper overloading for all the function, right?**So, now we know that we can kind-of implement overloading in Python, so how exactly do we do that?**  Single DispatchAbove we spoke about Multiple Dispatch, but Python doesn't support this out-of-the box, or in other words Multiple Dispatch is not a feature of Python standard library. What is available to us however, is called Single Dispatch, so let's begin with this simpler case first.**The only actual difference between multi and single dispatch is number of arguments which we can overload.** So, for this implementation in standard library it's just one.The function (and decorator) that provides this feature is called [**singledispatch**](https://docs.python.org/3/library/functools.htmlfunctools.singledispatch) and can be found in [**functools**](https://docs.python.org/3/library/functools.htmlmodule-functools) module.This whole concept is best explained with some examples. There are many "academic" examples of overloading functions (geometric shapes, addition, subtraction...) that we've probably all seen already. Rather than going over that, let's see some practical examples. So, here's first example for [singledispatch](https://docs.python.org/3/library/functools.htmlfunctools.singledispatch) to format dates, times and datetimes:<jupyter_code>from functools import singledispatch, singledispatchmethod
from datetime import date, datetime, time
@singledispatch
def format(arg):
    return arg

@format.register
def _(arg: date):
    return f"{arg.day}-{arg.month}-{arg.year}"

@format.register
def _(arg: datetime):
    return f"{arg.day}-{arg.month}-{arg.year} {arg.hour}:{arg.minute}:{arg.second}"

@format.register(time)
def _(arg):
    return f"{arg.hour}:{arg.minute}:{arg.second}"
# today
print(format("today"))

# 26-5-2021
print(format(date(2021, 5, 26)))

# 26-5-2021 17:25:10
print(format(datetime(2021, 5, 26, 17, 25, 10)))

# 19:22:15
print(format(time(19, 22, 15)))<jupyter_output>today
26-5-2021
26-5-2021 17:25:10
19:22:15<jupyter_text>We begin by defining the base `format` function that is going to be overloaded. This function is decorated with [**@singledispatch**](https://docs.python.org/3/library/functools.htmlfunctools.singledispatch) and provides base implementation, which is used if no better options is available. Next, we define individual functions for each type that we want to overload - in this case [**date**](https://docs.python.org/3/library/datetime.htmldate-objects), [**datetime**](https://docs.python.org/3/library/datetime.htmldatetime-objects) and [**time**](https://docs.python.org/3/library/datetime.htmltime-objects) - each of these have name `_` (underscore) because they will be called (dispatched) through the `format` method anyway, so no need to give them useful names. Each of them is also decorated with @format.register which attaches them to the previously mentioned format function. Then, to make it possible to differentiate between types, we have two options - we can use type annotations - as demonstrated in first two cases or explicitly add the type to decorator as with the last one from the example.**In some cases it might make sense to use same implementation for multiple types** - for example for number types such as `int` and `float` - for these situations decorator stacking is allowed, meaning that you can list (stack) multiple @format.register(type) lines to associate a function with all the valid types.Besides ability to overload basic functions, [**functools**](https://docs.python.org/3/library/functools.htmlmodule-functools) module contains also [**singledispatchmethod**](https://docs.python.org/3/library/functools.htmlfunctools.singledispatchmethod) that can be applied to methods of a class. Example of that could be the following:<jupyter_code>class Formatter:
    @singledispatchmethod
    def format(self, arg):
        raise NotImplementedError(f"Cannot format value of type {type(arg)}")

    @format.register
    def _(self, arg: date):
        return f"{arg.day}-{arg.month}-{arg.year}"

    @format.register
    def _(self, arg: time):
        return f"{arg.hour}:{arg.minute}:{arg.second}"
f = Formatter()

# 26-5-2021
print(f.format(date(2021, 5, 26)))

# 19:22:15
print(f.format(time(19, 22, 15)))<jupyter_output>26-5-2021
19:22:15<jupyter_text>Multiple DispatchOftentimes Single Dispatch won't be sufficient and you might need the proper Multiple Dispatch functionality. This is available from [**multipledispatch**](https://pypi.org/project/multipledispatch/) module which can be found here and can be installed with pip install multipledispatch.<jupyter_code>!pip install multipledispatch<jupyter_output>Collecting multipledispatch
  Downloading multipledispatch-0.6.0-py3-none-any.whl (11 kB)
Requirement already satisfied: six in /usr/lib/python3/dist-packages (from multipledispatch) (1.14.0)
Installing collected packages: multipledispatch
Successfully installed multipledispatch-0.6.0<jupyter_text>This module and it's decorator - @dispatch, behaves very similarly to the [**@singledispatch**](https://docs.python.org/3/library/functools.htmlfunctools.singledispatch) in the standard library. Only actual difference is that it can take multiple types as arguments:<jupyter_code>from multipledispatch import dispatch
@dispatch(list, str)
def concatenate(a, b):
    a.append(b)
    return a

@dispatch(str, str)
def concatenate(a, b):
    return a + b

@dispatch(str, int)
def concatenate(a, b):
    return a + str(b)
# ['a', 'b', 'c']
print(concatenate(["a", "b"], "c"))

# HelloWorld
print(concatenate("Hello", "World"))

# a1
print(concatenate("a", 1))<jupyter_output>['a', 'b', 'c']
HelloWorld
a1<jupyter_text>The above snippet shows how we can use @dispatch decorator to overload multiple arguments, for example to implement `concatenation` of various types. As you probably noticed, with [**multipledispatch**](https://pypi.org/project/multipledispatch/) library we didn't need to define and register base function, rather we created multiple functions with same name. **If we wanted to provide base implementation, we could use @dispatch(object, object) which would catch any non-specific argument types**.The previous examples shows proof-of-concept, but if we wanted to really implement such `concatenate` function, we would need to make it much more generic. This can be solved with use of [**union**](https://docs.python.org/3/library/typing.htmltyping.Union) types. In this specific example we could change the first function as follows:<jupyter_code>@dispatch((list, tuple), (str, int))
def concatenate(a, b):
    print(f'concatenate a={a} with b={b}')
    return list(a) + [b]
# # ['a', 'b', 'c']
print(concatenate(["a", "b"], "c"))

# ['a', 'b', 1]
print(concatenate(("a", "b"), 1))<jupyter_output>concatenate a=['a', 'b'] with b=c
['a', 'b', 'c']
concatenate a=('a', 'b') with b=1
['a', 'b', 1]<jupyter_text>This would make it so that first argument of the function could be any of `list` or `tuple`, while second one would be `str` or `int`. This is already much better than the previous solution, but it can be further improved using abstract types. Instead of listing all the possible sequences, we can use [**Sequence**](https://docs.python.org/3/library/typing.htmltyping.Sequence) abstract type (assuming that our implementation can handle it) which covers things like `list`, `tuple` or `range`:<jupyter_code>from collections.abc import Sequence
@dispatch(Sequence, (str, int))
def concatenate(a, b):
    print(f"Concatenate sequence a={a} with b={b}")
    return list(a) + [b]
print(concatenate(range(0, 10), 1))<jupyter_output>Concatenate sequence a=range(0, 10) with b=1
[0, 1, 2, 3, 4, 5, 6, 7, 8, 9, 1]<jupyter_text>If you want to take this approach, then it's good to take a look at [**collections.abc**](https://docs.python.org/3/library/collections.abc.html) module and see which container data-type best suits your needs. Mostly to make sure that your function will be able to handle all the types that fall into the chosen container.All this mixing and matching of argument types is convenient, but can also cause ambiguities when choosing suitable function for some specific set of parameters. Fortunately, [**multipledispatch**](https://pypi.org/project/multipledispatch/) provides AmbiguityWarning which is raised if ambiguous behaviour is possible:<jupyter_code>@dispatch(str, str)
def some_func(a, b):
    return 'p1'

@dispatch(str, range)
def some_func(a, b):
    return 'p2'

@dispatch(str, object)
def some_func(a, b):
    return 'p3'
assert some_func('a', 'b') == 'p1'
assert some_func('a', range(1,2)) == 'p2'
assert some_func('a', lambda a: a) == 'p3'<jupyter_output><empty_output>
{'fraction_non_alphanumeric': 0.10587147480351364, 'fraction_numerical': 0.016828478964401296, 'mean_word_length': 5.3623529411764705, 'pattern_counts': {'":': 0, '<': 23, '<?xml version=': 0, '>': 23, 'https://': 19, 'lorem ipsum': 0, 'www.': 0, 'xml': 0}, 'pii_count': 0, 'substrings_counts': 0, 'word_list_counts': {'cursed_substrings.json': 1, 'profanity_word_list.json': 0, 'sexual_word_list.json': 0, 'zh_pornsignals.json': 0}}
{'chain_length': '7', 'dir': 'jupyter-structured-clean-dedup', 'id': '346060', 'license': 'MIT', 'n_tokens_mistral': 3251, 'n_tokens_neox': 3060, 'n_words': 1254, 'path': 'article_collection/The_correct_way_to_overload_functions_in_python/notebook.ipynb', 'repo_name': 'johnklee/python_101_crash_courses', 'size': '10815'}