Dict-Based Dispatch

Concepts covered: Dict Methods

Ingestion code often turns raw text into typed values according to a declared column type. Written as a chain, each new type adds 2 lines plus another comparison that every later type has to pass through first.

if col_type == "int":
    value = int(raw)
elif col_type == "float":
    value = float(raw)
elif col_type == "str":
    value = str(raw)
else:
    value = None

Every test compares the same variable against a constant with ==. That shape is a lookup: given a key, find its value. A dictionary stores exactly that pairing.

The dictionary operations dispatch needs

4 operations cover dispatch

Values can be any object, including functions. int is a function you have called since type conversions. Written without parentheses, it is a value like 3 or "int", so a dict can store it. CASTERS["float"] evaluates to the float function, and the (raw) after it makes the call.

CASTERS = {"int": int, "float": float, "str": str}
raw = "19.99"
print(CASTERS["float"](raw))
print(CASTERS["str"](raw))
print(CASTERS.get("decimal"))
print("int" in CASTERS)
try:
    print(CASTERS["int"](raw))
except ValueError as err:
    print("ValueError:", err)

Output:

19.99
19.99
None
True
ValueError: invalid literal for int() with base 10: '19.99'

The last lookup shows that the dict only picks the converter: whether int accepts the text is still up to int. To replace the 8-line chain, fetch the converter first and call it only when the fetch found one:

caster = CASTERS.get(col_type)
if caster is None:
    value = None
else:
    value = caster(raw)

The is None test plays the chain's else. Writing CASTERS.get(col_type)(raw) without it turns an unknown type such as "decimal" into None(raw), which raises TypeError: 'NoneType' object is not callable where the chain set None. Whatever .get returns is called next, so a default for this pattern has to be something you can call.

Where a chain and a dict disagree

if-elif chain
  • Tests run top to bottom and the first True branch wins.
  • Each branch can test any expression, including a range or several variables at once.
  • A repeated condition leaves a dead branch below the first copy.
  • The else branch handles everything unmatched.
dict dispatch
  • Each key maps to a single value, so there's no order to win by.
  • Matches a key exactly with ==; ranges need another shape.
  • A repeated key keeps the last value and drops the earlier silently.
  • .get with a default handles everything unmatched.
LABELS = {"int": "integer", "float": "decimal", "int": "whole number"}
print(LABELS["int"])
print(LABELS)

Output:

whole number
{'int': 'whole number', 'float': 'decimal'}

A duplicated condition in a chain and a duplicated key in a dict fail in opposite directions. The chain keeps the first, the dict keeps the last, and neither warns. Tiers decided by amount >= 1000 stay in an if-elif chain, since no single key stands for every amount above a threshold.

Dispatching to a lookup

Adding "bool": bool to the casters looks consistent and breaks quietly. bool on a string tests truthiness, and every non-empty string is truthy, so the text "false" becomes True. The fix is a second dict whose .get method acts as the caster. Written without parentheses, BOOL_TEXT.get is a callable value in the same way int is.

BOOL_TEXT = {"true": True, "false": False}
CASTERS = {"int": int, "float": float, "bool": BOOL_TEXT.get}
print(bool("false"))
print(CASTERS["bool"]("false"))
print(CASTERS["bool"]("maybe"))

Output:

True
False
None

Unrecognised text such as "maybe" comes back as None. A later check can count that as a bad value instead of loading it as True.

What does this program print?

ROUTES = {"csv": "parse_rows", "json": "parse_doc", "csv": "parse_text"}
fmt = "xml"
print(ROUTES.get(fmt, "reject"), ROUTES["csv"])

One callable default suits columns whose type has no converter: str. Called on a string, it returns the same text. CASTERS.get(col_type, str)(raw) fetches the converter and calls it in one expression, so a column of undeclared type loads as plain text instead of dropping to None.

About this lesson

This section is part of the Control Flow: Advanced lesson on DataDriven, the home of data engineering. Lessons cover SQL, Python, Spark, data modeling and pipeline architecture, and each section pairs its explanation with worked examples and practice you run as you go.