Generic functions with singledispatch

functools · Advanced · 11 min

Serialising, formatting or validating many types without long isinstance chains keeps code open to extension.

@singledispatch turns a function into a generic function: you register implementations for different types, and the call dispatches on the type of the first argument.

@singledispatch
def to_json(obj):
    raise TypeError(f"can't serialise {type(obj).__name__}")

@to_json.register
def _(obj: datetime):
    return obj.isoformat()

Registration options:

  • Annotate the first parameter and use @f.register (shown above). Since 3.11, union types like int | float register several types at once.
  • Pass the type explicitly: @f.register(Decimal), or f.register(Path, str) for an existing function.

Dispatch follows the MRO: a registered base class handles subclasses unless a more specific registration exists. bool goes to an int implementation if there's no bool one. ABCs work too: registering for collections.abc.Mapping covers dicts and other mappings.

The big advantage over if isinstance(...) chains is extensibility: other modules can register implementations for their own types without editing your function.

f.registry maps types to implementations and f.dispatch(cls) shows which one would be used. For methods, use singledispatchmethod, which dispatches on the first argument after self.

Example

from collections.abc import Mapping
from datetime import date
from decimal import Decimal
from functools import singledispatch

@singledispatch
def render(obj):
    return repr(obj)                       # fallback for unknown types
@render.register
def _(obj: int | float):                   # union registration, 3.11+
    return f"{obj:,}"

@render.register
def _(obj: date):
    return obj.strftime("%d %b %Y")

@render.register(Mapping)
def _(obj):
    return ", ".join(f"{k}={render(v)}" for k, v in obj.items())

print(render(1234567))                     # 1,234,567
print(render({"due": date(2025, 3, 1), "total": 1500.5}))
# due=01 Mar 2025, total=1,500.5
print(render(True), render(Decimal("2")))  # 1 Decimal('2')  (bool -> int impl)
print(render.dispatch(bool).__name__)      # _

Remember

  • @singledispatch picks an implementation by the first argument's type
  • Register with annotations, explicit types or unions (3.11+); dispatch follows the MRO
  • Other modules can extend it without editing your code

Watch out for

  • Expecting dispatch on the second argument; singledispatch only looks at the first.

Check yourself

  1. render has implementations for object (the fallback) and int. Which one handles render(True)?

Sign in to answer, see the explanations, and get this lesson back as a quick review later.

Official docs: docs.python.org/3/library/functools.html#functools.singledispatch