Card 07/ 09

GotchaDifficulty: Advanced2 min

A Subclass That Overrides the Template Method Itself

The subclass replaces the whole sequence, skips validation, and nothing stops it in Python.

The template method is the one part of the pattern a subclass must not replace, because it is what guarantees the order of the steps. A subclass that overrides it can drop a step, and in Python and TypeScript nothing stops it when the program runs. This card shows a subclass skipping validation, how Java refuses the same thing, and how to guard against it in Python.

A developer wants a faster JSON import. Instead of writing parse alone, they override run to parse and store in one line, and the base class's run is marked @final.

py
class FastJsonImporter(Importer):
    def run(self, source: str) -> None:
        self.stored.extend(self.parse(source))

The input holds Kim, with an email, and Lee, without one, and the base class's validate rejects rows with no email.

Which rows are stored? Decide, then look.
py
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:
        rows = self.parse(source)
        self.stored.extend(row for row in rows if self.validate(row))

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

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


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

    def run(self, source: str) -> None:
        self.stored.extend(self.parse(source))


importer = FastJsonImporter()
importer.run('[{"name": "Kim", "email": "[email protected]"}, {"name": "Lee", "email": ""}]')
print("stored:", [row["name"] for row in importer.stored])
OutputRan with python:3.12-slim (Python 3.12.13)
stored: ['Kim', 'Lee']
`@final` did not stop the override, and Lee is stored without an email.

Both rows are stored. FastJsonImporter.run replaced the template method, so validate was never called, and @final changed nothing at run time.

The cost is a rule that every import was meant to follow, broken by one subclass, with no error. The same code in Java does not compile.

java
public class TmFinal {
    public static void main(String[] args) {
        new Sneaky().templateMethod();
    }
}

abstract class Base {
    public final void templateMethod() {
        System.out.println("base: the fixed order");
    }
}

class Sneaky extends Base {
    public void templateMethod() {
        System.out.println("sneaky: my own order");
    }
}
OutputRan with eclipse-temurin:21-jdk (Java 21.0.12)
java/TmFinal.java:14: error: templateMethod() in Sneaky cannot override templateMethod() in Base
    public void templateMethod() {
                ^
  overridden method is final
1 error
error: compilation failed
EXIT 1
The Java compiler refuses to override a `final` method.

In Python the guard is a type checker. typing.final, defined in PEP 591, tells a checker such as mypy to report any subclass that overrides the method, so running one in continuous integration turns the mistake into a failed build. Without one, name the template method clearly, say in the class's documentation which methods subclasses should write, and test that every importer validates.