With an application framework, you build a new application by reusing existing classes and overriding one or more methods to customize the behavior. At the heart of a framework is the Template Method: a method, defined in the base class, that drives the application by calling other base-class methods, some of which you override.
Python’s own unittest is an application
framework of this kind. You subclass TestCase and
supply setUp(), your test_* methods,
and tearDown(). The framework’s runner is the
template method. It calls setUp(), then your test,
then tearDown(), for each test, and you never call
that sequence yourself.
The defining trait of a Template Method is that the base
class fixes the shape of the algorithm. Subclasses
complete the individual steps. The @final decorator
from typing locks the template method so a subclass
cannot change the overall flow (see Making a
Class Final). Here, @final marks
run(), so the checker rejects any subclass that
overrides it, while leaving the step methods open:
# template_method.py
from typing import final, override
class ApplicationFramework:
def __init__(self) -> None:
self.run()
# The fixed algorithm. Subclasses supply the steps, not the flow:
@final
def run(self) -> None:
for _ in range(2):
self.customize1()
self.customize2()
def customize1(self) -> None: ...
def customize2(self) -> None: ...
# Create an "application" by filling in the steps:
class MyApp(ApplicationFramework):
@override
def customize1(self) -> None:
print("Nudge, nudge, wink, wink!")
@override
def customize2(self) -> None:
print("Say no more, say no more!")
MyApp()
#: Nudge, nudge, wink, wink!
#: Say no more, say no more!
#: Nudge, nudge, wink, wink!
#: Say no more, say no more!The base-class constructor starts the engine
(run()), which drives the application. The client
supplies customize1() and
customize2(), and the application runs. In a GUI
program that engine is the main event loop.
Notice which direction the calls flow. run()
lives in the base class, yet self.customize1()
executes MyApp’s version, because attribute lookup
on self starts at the object’s actual type, and
self is a MyApp. The base class calls
code written after it, sometimes years after. Framework authors
call this the Hollywood Principle: don’t call us, we’ll
call you. Your code supplies the steps; the framework decides
when they run.
A caution about the @final lock: it binds only
under the type checker. At runtime Python ignores it, and a
subclass override of run() replaces the fixed
algorithm without complaint. The guarantee is real, but it is
the checker’s guarantee, one more reason to make ty
or another checker part of the build.
The step methods default to ..., doing nothing,
so a subclass overrides only the steps it cares about, and a
forgotten step silently does nothing. When every subclass must
supply a step, inherit from ABC and declare the
step with @abstractmethod, as in Rethinking
Objects; then Python refuses to instantiate a subclass that
forgot it.
Starting the engine from the constructor carries a trap.
run() calls methods the subclass supplies, so a
subclass that defines its own __init__() must
finish its setup before it calls
super().__init__(). Call it first, in the usual
style, and the engine runs against half-initialized state:
# premature_engine.py
from typing import final, override
class Framework:
def __init__(self) -> None:
self.run()
@final
def run(self) -> None:
self.step()
def step(self) -> None: ...
class Greeter(Framework):
def __init__(self, name: str) -> None:
super().__init__() # Usual style: engine runs now...
self.name = name # ...before this line has happened
@override
def step(self) -> None:
print(f"Hello, {self.name}!")
try:
Greeter("Bruce")
except AttributeError as e:
print(type(e).__name__)
#: AttributeErrorGreeter("Bruce") never gets to greet.
super().__init__() starts the engine, the engine
calls step(), and step() reads
self.name one line before the constructor assigns
it. The quick fix is reordering: assign self.name
first, then call super().__init__(). That works,
but it inverts the convention every Python programmer carries,
and the next subclass author will restore the usual order
without thinking. The reliable fix changes the framework:
separate construction from starting, and have the client call
run() explicitly on a fully built object.
This pattern leans on the Liskov
Substitution Principle. A subclass must work wherever code
expects its base class. The base run() calls
customize1() and customize2() through
self, trusting that whatever a subclass supplies
still fits the algorithm’s shape. An override that breaks that
trust, doing nothing the flow relies on, or raising an exception
where the base would not, corrupts the fixed algorithm even
though the code still type-checks. The Template Method works
only when every subclass is a faithful substitute for its
base.
The test supplies a recording subclass and verifies the fixed flow:
# test_template_method.py
from typing import override
from template_method import ApplicationFramework
def test_template_method_runs_steps_in_order() -> None:
calls: list[str] = []
class Recorder(ApplicationFramework):
@override
def customize1(self) -> None:
calls.append("one")
@override
def customize2(self) -> None:
calls.append("two")
Recorder() # Constructing it runs the framework
assert calls == ["one", "two", "one", "two"] # Loop runs twiceSubclassing is one way to supply the varying steps, but not the only one. Because Python functions are first-class, you can pass the steps in directly, without using a subclass:
# template_function.py
from collections.abc import Callable
def run_framework(customize1: Callable[[], None],
customize2: Callable[[], None]) -> None:
for _ in range(2): # The fixed algorithm
customize1()
customize2()
run_framework(
lambda: print("Nudge, nudge, wink, wink!"),
lambda: print("Say no more, say no more!"),
)
#: Nudge, nudge, wink, wink!
#: Say no more, say no more!
#: Nudge, nudge, wink, wink!
#: Say no more, say no more!# test_template_function.py
from template_function import run_framework
def test_template_function_runs_steps_in_order() -> None:
calls: list[str] = []
run_framework(
lambda: calls.append("a"), lambda: calls.append("b"))
assert calls == ["a", "b", "a", "b"]Both the Template Method and the function version have a fixed algorithm and varying steps. If they share state, build on each other, or come as a coherent group, the subclass is clearer. If each step is independent, passing functions is lighter and avoids a class hierarchy. This is the same trade-off seen in Function Objects. A hook that holds no state is usually better as a function than as a method to override.
Create a framework that takes a list of file names. It opens every file but the last for reading, and the last one for writing. It processes each input file by an undetermined policy, and writes the output to the last file. Customize it two ways, once by subclassing and once by passing a function:
Fix premature_engine.py both ways: first
reorder the two lines in Greeter.__init__(), then
instead redesign Framework so clients construct the
object and call run() explicitly. Which fix still
protects a second subclass author who has never read this
chapter?