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
Jump to a chapter
Read the video transcript
Select a timestamp to watch that moment on YouTube.
- 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.
- 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.
- 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.
- 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.
- 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
- 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).
- 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).
- 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.
- 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.
- 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
print(isinstance(True,int))
print(type(True) is int)True
FalseSubclass checks and exact type checks answer different questions.
02. Register a bool handler
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))bool intA specific bool registration wins for boolean inputs.
03. Use the default fallback
from functools import singledispatch
@singledispatch
def kind(value): return 'other'
@kind.register
def _(value: str): return 'text'
print(kind('hi'), kind(3.5))text otherUnregistered types use the generic function body.
A closer look
Follow the reasoning, inspect each result, then try the suggested changes in the console below.
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.
print(isinstance(True,int))
print(type(True) is int)True
False- Run both type checks.
- Predict which branch receives True.
- Name the exact type-specific behavior required.
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.
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))bool int- Define the generic fallback.
- Register int and bool handlers.
- Call the same generic function with two different types.
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.
from functools import singledispatch
@singledispatch
def kind(value): return 'other'
@kind.register
def _(value: str): return 'text'
print(kind('hi'), kind(3.5))text other- Call with a registered str.
- Call with an unregistered float.
- 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 runNeed input()? Add one value per line
Your output appears here.
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.
Typical mistakes
Everyone meets these errors. See what causes them and how to fix them.
Broad int check before bool
if isinstance(value, int): ...Register bool separatelyWhat happens: True is formatted as an integer.
The most specific registered type handles booleans.
Assuming multiple dispatch
@singledispatch def combine(left, right): ...Dispatch on one explicit first argumentWhat happens: The second argument's type does not select an implementation.
Use other design patterns if both argument types determine behavior.