Card 06/ 09

ComparisonDifficulty: Intermediate2 min

Template Method Subclasses vs Passing Functions in Python

The fixed sequence becomes a function, and the varying steps become arguments.

A template method can be written without inheritance: the fixed sequence becomes a plain function, and each varying step becomes an argument that takes a function. This card writes the importer that way and sets out what each form gives you.

py
import csv
import io
import json
from collections.abc import Callable

Row = dict[str, str]


def run_import(
    source: str,
    parse: Callable[[str], list[Row]],
    validate: Callable[[Row], bool] = lambda row: True,
) -> list[Row]:
    rows = parse(source.strip())
    valid = [row for row in rows if validate(row)]
    print(f"{len(rows)} read, {len(valid)} stored")
    return valid


def parse_csv(text: str) -> list[Row]:
    return list(csv.DictReader(io.StringIO(text)))


run_import("name,email\nSam,[email protected]\nAli,[email protected]", parse_csv)
run_import(
    '[{"name": "Kim", "email": "[email protected]"}, {"name": "Lee", "email": ""}]',
    json.loads,
    validate=lambda row: bool(row.get("email")),
)
OutputRan with python:3.12-slim (Python 3.12.13)
2 read, 2 stored
2 read, 1 stored
`json.loads` is passed in as the parser, with a lambda for validation.

The results match the class version: two rows stored from CSV, one from JSON. There are no importer classes. run_import fixes the order, parse is a required argument, and validate has a default, which plays the hook's part. The JSON parser is the standard library's json.loads, passed in as it is.

This is composition in place of inheritance, and it turns each step into a small strategy. The order is now impossible to override, because there is no method to override.

The two forms compared
ConcernSubclassesFunctions as arguments
Protecting the orderOnly by final in Java, or by conventionNothing to override
Choosing the stepsWhen the class is writtenWhen the function is called, and they can differ per call
Steps that share stateFields on self, available to every stepPassed as arguments, or held in a closure
Many stepsMany methods, each with a nameA long argument list that is easy to get wrong

Pass functions when there are one or two varying steps and they share no state, as in the importer. Use subclasses when there are many steps that share state through self, or when a framework must discover your code by its method names, as unittest finds every test_ method.