C.W.K.
Stream
Lesson 03 of 05 · published

Function Overloading Workarounds — singledispatch, overload, Match

~18 min · overloading, singledispatch, typing-overload, dispatch

Level 0Curious
0 XP0/93 lessons0/23 achievements
0/100 XP to next level100 XP to go0% complete

Python doesn't have function overloading — and that's intentional

In Java/C++, you can declare multiple functions with the same name and different parameter types; the compiler picks the right one. Python doesn't. Defining two def f(...) with the same name silently replaces the first. This is consistent with Python's dynamic-typing philosophy — types aren't known at definition time.

functools.singledispatch — type-based dispatch on the first argument

The standard library provides singledispatch for the most common case: dispatch on the type of the first argument. Decorate a generic function, then register specialized versions per type. Very useful for cleanly handling "different formats of the same logical operation" (process this dict, this list, this DataFrame).

typing.overload — types-only signatures for static checking

@typing.overload doesn't change runtime behavior at all. It's purely for type checkers and IDEs — you declare multiple signatures, then provide one real implementation. Useful when a function has multiple valid call shapes that produce different return types. Pure documentation + tool support.

match — runtime dispatch by structure

The match statement (covered in flow track) gives you the cleanest runtime dispatch when the choice is based on the structure of the value, not just its type. For overloading-by-shape, match is often the most readable answer.

Principle: If you find yourself writing if isinstance(x, A): ... elif isinstance(x, B): ..., that's overloading-by-type, and singledispatch is usually the cleaner expression. If the dispatch is on shape (a list of three things vs a dict with these keys), use match.

Code

singledispatch — the standard library answer·python
from functools import singledispatch

@singledispatch
def serialize(obj):
    raise NotImplementedError(f"can't serialize {type(obj).__name__}")

@serialize.register
def _(obj: dict):
    return f"dict with {len(obj)} keys"

@serialize.register
def _(obj: list):
    return f"list of {len(obj)} items"

@serialize.register
def _(obj: str):
    return f"string of length {len(obj)}"

print(serialize({"a": 1}))           # dict with 1 keys
print(serialize([1, 2, 3]))         # list of 3 items
print(serialize("hello"))           # string of length 5
typing.overload — signatures for the type checker·python
from typing import overload

@overload
def parse(input: str) -> dict: ...
@overload
def parse(input: bytes) -> dict: ...
@overload
def parse(input: dict) -> dict: ...

def parse(input):                    # the actual implementation
    if isinstance(input, str):
        import json
        return json.loads(input)
    if isinstance(input, bytes):
        return parse(input.decode())
    if isinstance(input, dict):
        return dict(input)
    raise TypeError("unsupported")

# Type checkers see all three signatures; runtime sees just the implementation
match — dispatch on structure·python
def process(message):
    match message:
        case {"type": "chat", "text": text}:
            return f"chat: {text}"
        case {"type": "command", "name": name, "args": [*args]}:
            return f"cmd: {name}({', '.join(map(str, args))})"
        case [first, *rest]:
            return f"list with first={first}, rest={rest}"
        case str() if len(message) < 100:
            return f"short string: {message}"
        case _:
            return "unhandled"

print(process({"type": "chat", "text": "hi"}))
print(process({"type": "command", "name": "add", "args": [1, 2]}))
print(process([10, 20, 30]))
print(process("abc"))
singledispatchmethod — for dispatch on a method's first argument·python
from functools import singledispatchmethod

class Renderer:
    @singledispatchmethod
    def render(self, content):
        raise NotImplementedError

    @render.register
    def _(self, content: str):
        return f"<p>{content}</p>"

    @render.register
    def _(self, content: list):
        return "<ul>" + "".join(f"<li>{x}</li>" for x in content) + "</ul>"

r = Renderer()
print(r.render("hello"))             # <p>hello</p>
print(r.render(["a", "b", "c"]))     # <ul><li>a</li><li>b</li><li>c</li></ul>

External links

Exercise

Implement an area function using singledispatch that handles three types: a dict {type: 'circle', radius: r} returns πr², a dict {type: 'rectangle', w, h} returns w*h, and a list of [width, height, depth] returns w*h*d (treat as a box). Register each variant separately. Test with all three input shapes plus an unsupported type. Then rewrite as a match statement and compare readability.

Progress

Progress is local-only — sign in to sync across devices.
Spotted a bug or have feedback on this page?Report an Issue

Comments 0

🔔 Reply notifications (sign in)
Sign inPlease sign in to comment.

No comments yet — be the first.