Card 04/ 09

ExampleDifficulty: Intermediate2 min

Template Method Example: Importing CSV and JSON Files

Open, parse, validate and store, where only parse differs per format.

An importer for several file formats is a natural Template Method, and this example maps the template onto it. Every import opens its source, parses it into rows, keeps the rows that pass validation, and stores them. Only parsing differs between CSV and JSON, and the JSON importer also overrides the validation hook to reject a row with no email.

The template's parts in the importer example
TemplateImporter example
AbstractClassImporter
Template methodrun, marked @final
Abstract stepparse
Steps with a defaultopen and store, which subclasses rarely change
Hookvalidate, which accepts every row unless overridden
ConcreteClassCsvImporter and JsonImporter
py
import csv
import io
import json
from abc import ABC, abstractmethod
from typing import final

Row = dict[str, str]


class Importer(ABC):
    def __init__(self) -> None:
        self.stored: list[Row] = []

    @final
    def run(self, source: str) -> None:
        text = self.open(source)
        rows = self.parse(text)
        valid = [row for row in rows if self.validate(row)]
        self.store(valid)
        print(f"{type(self).__name__}: {len(rows)} read, {len(valid)} stored")

    def open(self, source: str) -> str:
        return source.strip()

    @abstractmethod
    def parse(self, text: str) -> list[Row]: ...

    def validate(self, row: Row) -> bool:
        return True

    def store(self, rows: list[Row]) -> None:
        self.stored.extend(rows)


class CsvImporter(Importer):
    def parse(self, text: str) -> list[Row]:
        return list(csv.DictReader(io.StringIO(text)))


class JsonImporter(Importer):
    def parse(self, text: str) -> list[Row]:
        return json.loads(text)

    def validate(self, row: Row) -> bool:
        return bool(row.get("email"))


CsvImporter().run("""
name,email
Sam,[email protected]
Ali,[email protected]
""")
JsonImporter().run("""
[{"name": "Kim", "email": "[email protected]"}, {"name": "Lee", "email": ""}]
""")
OutputRan with python:3.12-slim (Python 3.12.13)
CsvImporter: 2 read, 2 stored
JsonImporter: 2 read, 1 stored
The same `run` for both formats; only JSON rejects a row.

The CSV import reads and stores two rows. The JSON import reads two and stores one, because its validate rejects Lee, whose email is empty. CsvImporter is three lines long: it writes the one step that differs and inherits the order, the opening and the storing.

The template method is also where a rule for every import belongs. Logging, a count of rejected rows, or a transaction around store would each be one edit to run, and every importer, including ones not yet written, would follow it.