LESSON 129 · DEEPER PYTHON30 min

Dispatch Functions by Type with singledispatch

Register type-specific handlers and distinguish bool from int.

Example check: Worked and guided code samples are checked against CPython 3.13. Practice uses Pyodide 0.29.5 (Python 3.13) in your browser. How we check lessons

Video lesson: Dispatch Functions by Type with singledispatch

The preview is stored on this site. YouTube loads only when you press Play. Watch on YouTube

Read the video transcript

Select a timestamp to watch that moment on YouTube.

  1. 00:00

    A formatter accepts integers, booleans, and text values. A chain of isinstance tests often works, but Python's bool is a subclass of int, so an integer branch may capture True before a boolean branch ever runs. functools.singledispatch lets a generic function register one implementation per first-argument type. Print both isinstance(True, int) and type(True) is int. The first is True because bool subclasses int; the second is False because the exact runtime type is bool. A chain that checks int first therefore can classify a boolean incorrectly, even though the programmer wrote a later bool branch.

  2. 00:41

    A boolean is easy to misclassify because Python allows it in integer contexts. We will not debate whether that inheritance is useful; we will observe its effect, then make the intended formatting behavior explicit. Before the first run, make a concrete prediction about the terminal output. A prediction turns a passive demonstration into a test: we can compare the actual result with the rule we thought the code followed. Keep the input small enough to inspect every value. Run the broken branch order and observe True described as an integer. Then use @singledispatch for a default handler and @describe.register for int, bool, and str.

  3. 01:23

    Runtime dispatch chooses the registered implementation that best matches the argument type, so a bool registration wins over int for True. This is not a defect in isinstance. Inheritance is precisely what it reports. The bug is assuming the checks are mutually exclusive and ordering them without considering subclasses. A small conditional could check bool first. The lesson uses singledispatch because it also shows a scalable way to keep handlers separate. The broken branch order is a real logical failure, even though the program exits successfully. True appears under an integer label.

  4. 02:03

    Predict that output before running so the surprising result has a clear explanation afterward. Watch the output as evidence rather than jumping directly to a replacement snippet. The broken program may exit cleanly while still violating its contract. We will name the exact input, the observed result, and the result we wanted before changing code. The dispatch key is only the first argument. If behavior depends on two independent arguments, singledispatch is not a general multiple-dispatch engine. Registrations are attached to the generic function, and a new registration can change future calls.

  5. 02:43

    Keep the output contract clear and test both subclasses and fallback objects. The generic function handles unknown values. Registering an int implementation and then a bool implementation gives the dispatcher enough information to choose the more specific match. Notice that the call site stays label(value); callers do not choose a handler or pass a type name themselves. A registration can use an annotation to identify the dispatched type, as the runnable code does. Keep the default function meaningful rather than raising a confusing error for every unfamiliar value. For a public API, decide whether an unsupported type should produce a

  6. 03:26

    fallback, raise TypeError, or be normalized before dispatch. The diagnostic run compares a subclass- aware test with exact type identity. Those are different questions. singledispatch uses registered types and chooses a specific boolean handler when we provide one. The intermediate run isolates one mechanism. It is not the entire solution, and that is intentional. A small diagnostic is easier to explain than a large corrected application because it shows which assumption failed and which Python behavior we can rely on. Use functools.singledispatch to implement describe(value).

  7. 04:07

    Return bool:True or bool:False for booleans, int:N for integers, str:TEXT for strings, and other for unsupported objects. The prepared run must print bool:True followed by int:4. Decorate the default function with @singledispatch, then register separate functions for bool, int, and str. The last example registers only a string handler, so a float uses the generic fallback. That makes the extension point clear: another module could register a float handler later, but until then the behavior is explicit. Keep tests for at least one supported and one unsupported type. The corrected interface remains describe(value).

  8. 04:50

    We can add registered implementations without changing callers. The generic body still matters: it defines what happens for an object with no registered handler. After the fix, compare the observed output with the original prediction. The code shown on screen is executed locally and the terminal pane replays that captured run. The linked lesson gives you a separate checked task, so you can apply the idea rather than only copy a slide. The checked task exposes an ordinary describe(value) interface. It prints bool:True and int:4 for the prepared run, returns text for strings, and uses a predictable fallback for unsupported objects.

  9. 05:33

    This is a useful extension point when independent modules add handlers, but a simple conditional may be clearer when there are only two tiny cases. singledispatch chooses according to the first positional argument only. It will not pick a handler based on a second value's type or on a return type. If the algorithm truly depends on two types, design that rule separately rather than piling surprising checks into one registered function. The most specific registered type handles booleans. Use other design patterns if both argument types determine behavior. Finally, compare False with zero and an unregistered object.

  10. 06:14

    Those three inputs reveal whether booleans are truly separate and whether the fallback still works. A second argument would not participate in this dispatch decision. An edge case is more valuable than repeating the same happy path. The final run challenges a boundary or invalid input and explains the decision the function makes. If your own implementation disagrees, use the lesson console to narrow the smallest failing case.

Understand the concept

A formatter accepts integers, booleans, and text values. A chain of isinstance tests often works, but Python's bool is a subclass of int, so an integer branch may capture True before a boolean branch ever runs. functools.singledispatch lets a generic function register one implementation per first-argument type.

Run the broken branch order and observe True described as an integer. Then use @singledispatch for a default handler and @describe.register for int, bool, and str. Runtime dispatch chooses the registered implementation that best matches the argument type, so a bool registration wins over int for True.

The dispatch key is only the first argument. If behavior depends on two independent arguments, singledispatch is not a general multiple-dispatch engine. Registrations are attached to the generic function, and a new registration can change future calls. Keep the output contract clear and test both subclasses and fallback objects.

The checked task exposes an ordinary describe(value) interface. It prints bool:True and int:4 for the prepared run, returns text for strings, and uses a predictable fallback for unsupported objects. This is a useful extension point when independent modules add handlers, but a simple conditional may be clearer when there are only two tiny cases.

  • bool is an int subclass
  • Dispatch considers the first argument
  • Register a specific bool handler
  • Keep a deliberate fallback

See it step by step

Read the code, predict the output, then compare it with the result.

01. Expose the bool subclass trap

PYTHON
print(isinstance(True,int))
print(type(True) is int)
EXPECTED OUTPUT
True
False

Subclass checks and exact type checks answer different questions.

02. Register a bool handler

PYTHON
from functools import singledispatch
@singledispatch
def label(value): return 'other'
@label.register
def _(value: int): return 'int'
@label.register
def _(value: bool): return 'bool'
print(label(True), label(2))
EXPECTED OUTPUT
bool int

A specific bool registration wins for boolean inputs.

03. Use the default fallback

PYTHON
from functools import singledispatch
@singledispatch
def kind(value): return 'other'
@kind.register
def _(value: str): return 'text'
print(kind('hi'), kind(3.5))
EXPECTED OUTPUT
text other

Unregistered types use the generic function body.

A closer look

Follow the reasoning, inspect each result, then try the suggested changes in the console below.

01 / 03

Inspect why bool matches an int check

Print both isinstance(True, int) and type(True) is int. The first is True because bool subclasses int; the second is False because the exact runtime type is bool. A chain that checks int first therefore can classify a boolean incorrectly, even though the programmer wrote a later bool branch.

This is not a defect in isinstance. Inheritance is precisely what it reports. The bug is assuming the checks are mutually exclusive and ordering them without considering subclasses. A small conditional could check bool first. The lesson uses singledispatch because it also shows a scalable way to keep handlers separate.

PYTHON
print(isinstance(True,int))
print(type(True) is int)
EXPECTED OUTPUT
True
False
Follow the reasoning
  1. Run both type checks.
  2. Predict which branch receives True.
  3. Name the exact type-specific behavior required.
02 / 03

Register distinct implementations

The generic function handles unknown values. Registering an int implementation and then a bool implementation gives the dispatcher enough information to choose the more specific match. Notice that the call site stays label(value); callers do not choose a handler or pass a type name themselves.

A registration can use an annotation to identify the dispatched type, as the runnable code does. Keep the default function meaningful rather than raising a confusing error for every unfamiliar value. For a public API, decide whether an unsupported type should produce a fallback, raise TypeError, or be normalized before dispatch.

PYTHON
from functools import singledispatch
@singledispatch
def label(value): return 'other'
@label.register
def _(value: int): return 'int'
@label.register
def _(value: bool): return 'bool'
print(label(True), label(2))
EXPECTED OUTPUT
bool int
Follow the reasoning
  1. Define the generic fallback.
  2. Register int and bool handlers.
  3. Call the same generic function with two different types.
03 / 03

Verify the fallback and one-type limit

The last example registers only a string handler, so a float uses the generic fallback. That makes the extension point clear: another module could register a float handler later, but until then the behavior is explicit. Keep tests for at least one supported and one unsupported type.

singledispatch chooses according to the first positional argument only. It will not pick a handler based on a second value's type or on a return type. If the algorithm truly depends on two types, design that rule separately rather than piling surprising checks into one registered function.

PYTHON
from functools import singledispatch
@singledispatch
def kind(value): return 'other'
@kind.register
def _(value: str): return 'text'
print(kind('hi'), kind(3.5))
EXPECTED OUTPUT
text other
Follow the reasoning
  1. Call with a registered str.
  2. Call with an unregistered float.
  3. Check bool and int separately in the task.

Try it in Python

Edit the example and run it. Python starts in your browser the first time you click Run.

Python console

Ready to run
Need input()? Add one value per line
Ctrl / ⌘ + Enter to run
OUTPUT
Your output appears here.
Your turnUse functools.singledispatch to implement describe(value). Return bool:True or bool:False for booleans, int:N for integers, str:TEXT for strings, and other for unsupported objects. The prepared run must print bool:True followed by int:4.
Need a hint?

Decorate the default function with @singledispatch, then register separate functions for bool, int, and str.

Complete the task and select Check task to verify your code.

    Quick quiz

    Three questions. You can change your answers and try again.

    01.Why can an int isinstance branch catch True?

    02.Which argument does singledispatch inspect?

    03.What handles an unregistered object?

    Typical mistakes

    Everyone meets these errors. See what causes them and how to fix them.

    Broad int check before bool

    COMMON MISTAKE
    if isinstance(value, int): ...
    FIX
    Register bool separately

    What happens: True is formatted as an integer.
    The most specific registered type handles booleans.

    Assuming multiple dispatch

    COMMON MISTAKE
    @singledispatch def combine(left, right): ...
    FIX
    Dispatch on one explicit first argument

    What happens: The second argument's type does not select an implementation.
    Use other design patterns if both argument types determine behavior.

    Browse this lesson’s problem fixes

    Sources and further reading