A function that accepts several kinds of input often begins with a few isinstance() checks. That approach is straightforward when the cases are small and local. As the number of supported types grows, however, one function can become a long decision tree that mixes unrelated implementations.

Python’s functools.singledispatch offers another design. It turns one function into a generic function whose implementation is selected from the runtime type of its first argument. Type-specific behavior can then be registered separately while callers keep using one public function.

Single dispatch is useful for serializers, renderers, adapters, formatters, and other operations where behavior naturally varies by one input type. It is not a replacement for every conditional or for normal object-oriented polymorphism.

Start with a generic function

Decorate the fallback implementation with @singledispatch:

from functools import singledispatch


@singledispatch
def render(value):
    return f"<{type(value).__name__}>"

The decorated function remains callable in the usual way. The original implementation becomes the fallback associated with object, so it handles values for which no more specific registration is available.

Add implementations with the generic function’s register attribute:

@render.register
def _(value: str):
    return value


@render.register
def _(value: int):
    return str(value)

Now the public call site does not need to choose an implementation:

render("ready")
render(42)
render(3.5)

The first two calls use their registered implementations. The float call falls back to the original function unless a compatible registration exists.

Dispatch uses only the first argument

The most important constraint is in the name: this is single dispatch.

Given a function such as:

@singledispatch
def convert(value, format_name, compact=False):
    ...

selection depends on the runtime type of value. The types or values of format_name and compact do not participate in dispatch.

If behavior depends equally on two independent runtime types, singledispatch is usually the wrong abstraction. An explicit lookup table, a different object model, or ordinary branching may express that relationship more clearly.

Let annotations identify registered types

When a registered implementation annotates its first parameter, register() can infer the dispatch type:

@render.register
def _(value: bytes):
    return value.hex()

You can also pass the dispatch type explicitly:

@render.register(list)
def render_list(value):
    return ", ".join(map(str, value))

The explicit form is useful when the function annotation describes more detail than runtime dispatch can use.

For example:

@render.register(list)
def render_names(value: list[str]):
    return ", ".join(value)

The runtime registration is for list. It does not distinguish list[str] from list[int]. Parameterized type hints describe element types for tools such as static type checkers; they do not make singledispatch inspect every element at runtime.

More specific types win

Dispatch is not limited to exact type matches. If the concrete type has no direct registration, Python can use its method resolution order to find a registered base type.

This makes abstract base classes especially useful. For example:

from collections.abc import Mapping
from functools import singledispatch


@singledispatch
def summarize(value):
    return repr(value)


@summarize.register
def _(value: Mapping):
    return ", ".join(f"{key}={item}" for key, item in value.items())

A normal dict can use the Mapping implementation even though dict itself was not registered.

Registering against an interface-like abstract base class can be preferable to registering many concrete container classes separately. It also means a later, more specific registration can change which implementation is selected, so registrations should be deliberate and tested.

Keep implementations independently testable

The register() decorator returns the undecorated implementation function rather than the generic dispatcher. That lets you give an implementation a real name and test it directly:

@render.register(float)
def render_float(value):
    return f"{value:.2f}"


assert render_float(1.25) == "1.25"
assert render(1.25) == "1.25"

Testing both levels can be valuable. A direct test checks the implementation logic, while a dispatcher test confirms that the expected runtime type reaches that implementation.

The generic function also exposes dispatch(type) for inspecting the implementation that would be selected for a type:

implementation = render.dispatch(float)
assert implementation is render_float

Its read-only registry attribute exposes the registered type-to-function mapping for inspection.

Use methods when the operation belongs to a class

For methods, Python provides functools.singledispatchmethod.

from functools import singledispatchmethod


class Normalizer:
    @singledispatchmethod
    def normalize(self, value):
        raise TypeError(f"unsupported type: {type(value).__name__}")

    @normalize.register
    def _(self, value: str):
        return value.strip()

    @normalize.register
    def _(self, value: bytes):
        return value.strip()

Method dispatch uses the first argument after self or cls. In this example, that is value.

singledispatchmethod can be combined with decorators such as classmethod, but decorator order matters: singledispatchmethod needs to be outermost so the resulting descriptor still exposes register.

Prefer a meaningful fallback

The base implementation is part of the design, not boilerplate.

For a renderer, a generic representation may be reasonable. For a conversion API, silently accepting an unsupported type may hide a bug. In that case, fail explicitly:

@singledispatch
def encode(value):
    raise TypeError(f"cannot encode {type(value).__name__}")

This keeps unsupported inputs visible and allows registered implementations to define the accepted surface deliberately.

Avoid returning None merely because no implementation exists unless None is a documented result of the operation.

Avoid registrations with surprising semantics

Dispatch based on broad types can create unintuitive behavior.

For example, bool is a subclass of int in Python. If you register only an int implementation, Boolean values can reach it through normal type hierarchy rules:

@singledispatch
def describe(value):
    return "other"


@describe.register
def _(value: int):
    return "integer"


assert describe(True) == "integer"

If Boolean values need different semantics, register bool explicitly.

Similar surprises can arise with broad abstract base classes. Before registering a general interface, consider all types that satisfy it, not just the concrete type that motivated the registration.

Do not confuse dispatch with static overloads

singledispatch changes runtime behavior. It selects and calls an implementation while the program is running.

Type-checking overloads serve a different purpose. typing.overload describes alternative call signatures to static type checkers; the overload declarations themselves do not provide runtime dispatch among implementations.

A project may use both mechanisms, but they solve different problems. Do not add singledispatch merely to make a type checker understand several signatures, and do not assume overload declarations will select runtime implementations.

Know when a method is simpler

If you control the input classes and the operation is intrinsic to those objects, ordinary polymorphism may be clearer:

result = document.render()

Single dispatch is especially attractive when the operation is external to the classes, when you cannot modify those classes, or when extensions should register behavior without editing a central conditional.

There is a trade-off. Registrations distribute behavior across functions or modules, which reduces one large conditional but can make the complete set of supported types less obvious. Keep registrations discoverable and avoid scattering them through unrelated import side effects.

Avoid using dispatch as validation

A registered type says which implementation should run; it does not prove that the value is valid for the operation.

A str implementation may still need to reject empty values or malformed content. A Mapping implementation may require particular keys. Keep those domain checks inside the relevant implementation rather than expecting the dispatch layer to enforce them.

Likewise, dispatching on a class does not establish trust. Untrusted input still needs normal validation and security controls before it reaches sensitive operations.

Common pitfalls

Dispatching on the wrong parameter

Only the first argument participates in singledispatch. Reordering parameters later can therefore change the design, not just the call syntax.

Expecting generic type arguments to dispatch

list[str] and list[int] are not separate runtime dispatch cases for singledispatch. Register the runtime container type and validate or process its contents inside the implementation.

Registering every concrete subclass

If several types share a meaningful abstract base class and the same behavior, one registration against that abstraction can be easier to maintain.

Hiding unsupported values in the fallback

A permissive fallback can make accidental input types look valid. Raise a clear exception when unsupported values indicate a programming or data error.

Building an invisible plugin system

Runtime registration can support extensibility, but import order and hidden registration side effects can make a system difficult to understand. If third-party plugins are involved, define an explicit loading and registration contract.

Use single dispatch when one type drives the operation

A good singledispatch design has a simple shape:

  1. one public operation;
  2. one first argument whose runtime type naturally determines behavior;
  3. a clear fallback policy;
  4. registrations against meaningful concrete types or abstractions;
  5. tests for both implementation logic and dispatch selection.

When those conditions hold, single dispatch can replace a growing isinstance() chain with an extensible standard-library mechanism. When behavior depends on several values, hidden application state, or classes you already control, a different design may be easier to read.

The value of singledispatch is not fewer lines of code by itself. It is a clearer separation between an operation’s public interface and the type-specific implementations that make that operation work.