Typorithm · medium

Single dispatch and subclass-specific decisions

pythonlibrariespackagingindependent-practice
Saved on this device
Full lesson · 4 learning steps Open any exercise for focused practice
1

Step 1 of 4 · intro card

Single dispatch and subclass-specific decisions

0%

Concept and contract

Dispatch on the first argument while keeping boolean behavior separate from integer behavior.

Single dispatch chooses an implementation from the runtime type of the first argument. It can make an operation with several supported input categories easier to extend than one long conditional. The default implementation remains important because unsupported categories still need a defined outcome.

The example normalizes integers, strings and booleans deliberately. Bool is a subclass of int, but this domain rejects booleans as scores through a more specific registered handler. This is an explicit semantic choice: dispatch uses type relationships, while the application decides what each matching type should mean.

Mechanism and worked example

The default normalize raises TypeError.

The default normalize raises TypeError. Registered integer values are doubled, strings are stripped and booleans reach their dedicated rejection branch. The outer reporting function converts only those deliberate TypeErrors into an unsupported marker while preserving input order.

The mixed case includes an integer, True, a padded string and None. It therefore exercises all four outcomes in a single observation. Empty input remains empty, and a zero integer distinguishes a valid numeric zero from False. Returning a marker is this report's presentation policy, not an inherent feature of singledispatch.

Complete worked example

python

from functools import singledispatch

@singledispatch
def normalize(value):
    raise TypeError('unsupported value')

@normalize.register
def _(value: int):
    return value * 2

@normalize.register
def _(value: bool):
    raise TypeError('boolean is not a score')

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

def inspect_example(values):
    result = []
    for value in values:
        try:
            result.append(normalize(value))
        except TypeError:
            result.append('unsupported')
    return result
Reasoning
The default normalize raises TypeError. Registered integer values are doubled, strings are stripped and booleans reach their dedicated rejection branch. The outer reporting function converts only those deliberate TypeErrors into an unsupported marker while preserving input order.
Download the reusable Python and packaging computer lab ↗

Focused decision: use the complete example for context

python

@normalize.register
def _(value: bool):
    raise TypeError("boolean is not a score")
Decision
Keep a dedicated bool rule while extending only the string branch to uppercase after trimming.
Download the reusable Python and packaging computer lab ↗

Counterexamples and common mistakes

The controlled bug adds two instead of multiplying by two in the integer branch.

The controlled bug adds two instead of multiplying by two in the integer branch. Registration still selects the correct branch, yet its transformation violates the contract. Dispatch architecture cannot establish the correctness of each handler's body.

Removing the bool specialization would create another defect: boolean values could fall through to the integer handler because of subclass relationships. Be careful when registering broad abstract types or adding another handler later. An extension can alter selection for existing values, so keep representative boundary cases alongside the apparently independent implementations.

Controlled bug: the executable contract rejects this version

python

from functools import singledispatch

@singledispatch
def normalize(value):
    raise TypeError('unsupported value')

@normalize.register
def _(value: int):
    return value + 2

@normalize.register
def _(value: bool):
    raise TypeError('boolean is not a score')

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

def inspect_example(values):
    result = []
    for value in values:
        try:
            result.append(normalize(value))
        except TypeError:
            result.append('unsupported')
    return result
Reasoning
The controlled bug adds two instead of multiplying by two in the integer branch. Registration still selects the correct branch, yet its transformation violates the contract. Dispatch architecture cannot establish the correctness of each handler's body.
Download the reusable Python and packaging computer lab ↗

Independent practice

Extend only the string handler to uppercase its stripped text.

Extend only the string handler to uppercase its stripped text. Preserve integer doubling, boolean rejection and unsupported defaults. The first hint is to find the str registration rather than adding an outer blanket conversion over every result.

The second hint is to use the accented-character case as well as the padded ASCII case. The output should follow Python's actual string uppercasing behavior. Do not uppercase the unsupported marker or convert numeric values into strings: this exercise adds one type-specific transformation while keeping the heterogeneous report contract stable.

Step 1: Keep a dedicated bool rule while extending only the string branch to uppercase after trimming. Step 2: Run the accepted baseline, edit the exercise, then check its independent solution cases; preserve caller inputs and the documented exception behavior.

Practice starter: solve before reading the solution

python

from functools import singledispatch

@singledispatch
def normalize(value):
    raise TypeError('unsupported value')

@normalize.register
def _(value: int):
    return value * 2

@normalize.register
def _(value: bool):
    raise TypeError('boolean is not a score')

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

def inspect_example(values):
    result = []
    for value in values:
        try:
            result.append(normalize(value))
        except TypeError:
            result.append('unsupported')
    return result

# Task: Keep a dedicated bool rule while extending only the string branch to uppercase after trimming.
# Practice contract: Keep a dedicated bool rule while extending only the string branch to uppercase after trimming.
Reasoning
Keep a dedicated bool rule while extending only the string branch to uppercase after trimming.
Download the reusable Python and packaging computer lab ↗
  • Hint 1: Extend only the string handler to uppercase its stripped text. Preserve integer doubling, boolean rejection and unsupported defaults. The first hint is to find the str registration rather than adding an outer blanket conversion over every result.
  • Hint 2: The second hint is to use the accented-character case as well as the padded ASCII case. The output should follow Python's actual string uppercasing behavior. Do not uppercase the unsupported marker or convert numeric values into strings: this exercise adds one type-specific transformation while keeping the heterogeneous report contract stable.

Solution, tests and trade-offs

The solution chains upper after strip in the registered string function.

The solution chains upper after strip in the registered string function. Acceptance checks that the mixed report still contains a numeric product and explicit unsupported markers, while its string element changes as requested. The empty case protects the collection boundary.

Single dispatch works best when the operation genuinely varies by the first argument's type. If selection depends on several fields, configuration or business rules, explicit strategies or ordinary conditionals may communicate intent better. Treat registration as part of the operation's public behavior and review how new subclass relationships affect previously accepted inputs.

Practice solution and acceptance checks

python

from functools import singledispatch

@singledispatch
def normalize(value):
    raise TypeError('unsupported value')

@normalize.register
def _(value: int):
    return value * 2

@normalize.register
def _(value: bool):
    raise TypeError('boolean is not a score')

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

def inspect_example(values):
    result = []
    for value in values:
        try:
            result.append(normalize(value))
        except TypeError:
            result.append('unsupported')
    return result
Reasoning
The solution chains upper after strip in the registered string function. Acceptance checks that the mixed report still contains a numeric product and explicit unsupported markers, while its string element changes as requested. The empty case protects the collection boundary.
Download the reusable Python and packaging computer lab ↗

Interview reasoning and next step

Explain why True and one can reach different handlers despite their numeric relationship.

Explain why True and one can reach different handlers despite their numeric relationship. Then identify what the generic default contributes and what the outer TypeError handler changes in the returned report. Keep dispatch selection distinct from the correctness of a handler's calculation.

For a stretch, add a new owned class and register one explicit implementation. Write cases for that class, one subclass and an unrelated object before running the report. Inspect the selected handler when useful, but retain end-to-end result checks because selecting the intended function does not prove that function honors the contract.

Optional stretch — after the core task works: For a stretch, add a new owned class and register one explicit implementation. Write cases for that class, one subclass and an unrelated object before running the report. Inspect the selected handler when useful, but retain end-to-end result checks because selecting the intended function does not prove that function honors the contract.

Computer practice: https://applydjinn.com/typorithm/python-engineering-lab-20261107.zip. Use an isolated Python 3.13 environment, install requirements.txt, then run verify.py. Edit exercises/pye-dispatch.py and run --practice pye-dispatch. Fixture files are created in fresh temporary directories for every case. Packaging checks build real local artifacts, install the exact wheel without an index and run outside the source tree. No package is published to PyPI. The app includes static examples, prepared results and questions; it does not run Python or provide an editor.

Dispatch on the first argument while keeping boolean behavior separate from integer behavior.