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 a policy the customization supplies, and writes the output to the last file. Supply each of these policies twice, once by subclassing and once by passing a function:
- Convert all the letters in each file to uppercase.
- Treat the first file as a list of search words, one per line, and report which of those words appear in each remaining input file.
Passing
the Steps as Functions shows the same algorithm anchored in
a function that takes the varying step as an argument. The
subclass form is in The
Anchored Algorithm. Put the fixed loop (read each input,
apply the step, write the output) in one run() and
one function, and leave process() as the one open
step. The search policy needs the word list, so store it on the
subclass and close over it in the function form.
# The shape of exercise_1.py
from abc import ABC, abstractmethod
from collections.abc import Callable
from pathlib import Path
from typing import final
class FileFramework(ABC):
__slots__ = ()
@final
def run(self, filenames: list[str]) -> None:
...
@abstractmethod
def process(self, text: str) -> str: ...
def run_file_framework(
filenames: list[str], process: Callable[[str], str]
) -> None:
...# The shape of exercise_1_upper.py
import tempfile
from pathlib import Path
from typing import override
from exercise_1 import FileFramework, run_file_framework
class Uppercase(FileFramework):
@override
def process(self, text: str) -> str:
...
def demo() -> None:
...# The shape of exercise_1_search.py
import tempfile
from pathlib import Path
from typing import override
from exercise_1 import FileFramework, run_file_framework
from record import record
def found(words: list[str], text: str) -> str:
...
@record
class Search(FileFramework):
words: list[str]
@override
def process(self, text: str) -> str:
...
def demo() -> None:
...The framework anchors the shape: read every file but the
last, run the varying process() step over each
one’s text, and write the combined result to the last file. It
appears twice, as a base class whose run() is the
template method and as a function that takes the step as an
argument. Every customization must supply
process(), so the base class declares it with
@abstractmethod:
# exercise_1.py
from abc import ABC, abstractmethod
from collections.abc import Callable
from pathlib import Path
from typing import final
class FileFramework(ABC):
__slots__ = ()
@final
def run(self, filenames: list[str]) -> None:
*inputs, output = filenames
pieces = [
self.process(Path(name).read_text())
for name in inputs]
Path(output).write_text("".join(pieces))
@abstractmethod
def process(self, text: str) -> str: ...
def run_file_framework(
filenames: list[str], process: Callable[[str], str]
) -> None:
*inputs, output = filenames
pieces = [process(Path(name).read_text())
for name in inputs]
Path(output).write_text("".join(pieces))Let a subclass be a record. The empty
__slots__ lets a subclass that carries data be a
record, as Rethinking
Objects explains.
The uppercase policy needs no data. The subclass overrides
process(), and the function form passes
str.upper, which has the signature the framework
calls:
# exercise_1_upper.py
import tempfile
from pathlib import Path
from typing import override
from exercise_1 import FileFramework, run_file_framework
class Uppercase(FileFramework):
@override
def process(self, text: str) -> str:
return text.upper()
def demo() -> None:
with tempfile.TemporaryDirectory() as tmp:
root = Path(tmp)
(root / "a.txt").write_text("hello\n")
(root / "b.txt").write_text("world\n")
inputs = [str(root / "a.txt"), str(root / "b.txt")]
# Subclassing customization:
out1 = root / "out1.txt"
Uppercase().run([*inputs, str(out1)])
print(repr(out1.read_text()))
# Function-passing customization:
out2 = root / "out2.txt"
run_file_framework([*inputs, str(out2)], str.upper)
print(repr(out2.read_text()))
demo()
#: 'HELLO\nWORLD\n'
#: 'HELLO\nWORLD\n'Both produce 'HELLO\nWORLD\n', because both
supply the same step through two different mechanisms. The
anchored algorithm, “read every input, transform it, concatenate
into the output,” lives in one place either way: the base
class’s run(), or the function
run_file_framework().
The search policy needs the word list while it processes each file. The framework treats every input alike, so the client reads the word list from the first file and hands the framework the rest:
# exercise_1_search.py
import tempfile
from pathlib import Path
from typing import override
from exercise_1 import FileFramework, run_file_framework
from record import record
def found(words: list[str], text: str) -> str:
present = [w for w in words if w in text.split()]
return f"{' '.join(present)}\n"
@record
class Search(FileFramework):
words: list[str]
@override
def process(self, text: str) -> str:
return found(self.words, text)
def demo() -> None:
with tempfile.TemporaryDirectory() as tmp:
root = Path(tmp)
wordfile = root / "words.txt"
wordfile.write_text("spam\neggs\nham\n")
(root / "a.txt").write_text("spam and eggs\n")
(root / "b.txt").write_text("green eggs and ham\n")
inputs = [str(root / "a.txt"), str(root / "b.txt")]
words = wordfile.read_text().split()
# Subclassing customization:
out1 = root / "out1.txt"
Search(words).run([*inputs, str(out1)])
print(repr(out1.read_text()))
# Function-passing customization:
out2 = root / "out2.txt"
run_file_framework(
[*inputs, str(out2)],
lambda text: found(words, text))
print(repr(out2.read_text()))
demo()
#: 'spam eggs\neggs ham\n'
#: 'spam eggs\neggs ham\n'The report has one line for each input file, naming the search words that file contains.
Give the step its data. The two forms differ
in where the word list lives. Search stores it in a
field, and the lambda closes over the local variable
words. In both, run() and
run_file_framework() stay unchanged: a new policy
is a new step, and the algorithm that calls the step belongs to
the framework.
Repair
premature_engine.pyboth ways: first reorder the two lines inGreeter.__init__(), then redesignFrameworkinstead, so clients construct the object and callrun()explicitly. Which repair still protects a second subclass author who has not read this chapter?
Don’t
Start the Engine in the Constructor shows
Framework.__init__() calling a step before the
subclass has set its attributes. One repair changes the order of
two lines in Greeter.__init__(). The other removes
the call from Framework.__init__() so that the
client calls run(). To choose between them,
consider who must remember what.
# The shape of exercise_2_reorder.py
from typing import final, override
class Framework:
def __init__(self) -> None:
...
@final
def run(self) -> None:
...
def step(self) -> None: ...
class Greeter(Framework):
def __init__(self, name: str) -> None:
...
@override
def step(self) -> None:
...# The shape of exercise_2_redesign.py
from typing import final, override
class Framework:
@final
def run(self) -> None: # No longer called from __init__
...
def step(self) -> None: ...
class Greeter(Framework):
def __init__(self, name: str) -> None:
...
@override
def step(self) -> None:
...If you drop the super().__init__() call from
Greeter.__init__() to avoid the trap,
Greeter("Brian") runs without an error and prints
nothing, because the engine starts in
Framework.__init__(), which no longer runs. The
type checker reports nothing either. The reorder keeps the call
and moves it after the setup, and the redesign takes the start
out of construction, so no subclass constructor decides whether
the engine runs.
The quick repair reorders the two lines so the subclass finishes its own setup before handing control to the base class:
# exercise_2_reorder.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:
self.name = name # Setup first...
super().__init__() # ...then start the engine
@override
def step(self) -> None:
print(f"Hello, {self.name}!")
Greeter("Brian")
#: Hello, Brian!The redesign removes the hazard instead of avoiding it.
Framework no longer runs anything during
construction, so the client builds a finished object and starts
it:
# exercise_2_redesign.py
from typing import final, override
class Framework:
@final
def run(self) -> None: # No longer called from __init__
self.step()
def step(self) -> None: ...
class Greeter(Framework):
def __init__(self, name: str) -> None:
self.name = name
@override
def step(self) -> None:
print(f"Hello, {self.name}!")
greeter = Greeter("Brian") # Construction starts nothing
greeter.run() # The client starts the engine
#: Hello, Brian!The redesign is the one that protects the next author. The
reorder works, but it works only for the subclass that performs
it, and it survives only as long as everyone remembers it. It
asks every future subclass author to invert the convention they
have used everywhere else, which is to call
super().__init__() first. Nothing in the signature
says so, and no type checker objects to the usual order. The
failure arrives as an AttributeError inside a base
class someone else wrote. A rule the next author must remember,
without having read this chapter, is not a repair.
Separating construction from starting makes the mistake
unavailable. No window exists in which the engine runs against
half-built state, because construction runs no engine. The extra
line at every call site, greeter.run(), moves the
decision about when the algorithm starts from the base class to
the code that knows the object is ready. The same reasoning
drives eager versus lazy construction in Singleton,
where the timing of a hidden step makes the difference.
run()Subclass
ApplicationFrameworkand overriderun()with a version that callscustomize2()beforecustomize1(). Run it, then runtyover it. Which of the two, Python or the type checker, objects to the change? What does that tell you about the source of the anchored algorithm’s guarantee?
Hooks
and the Misspelled Override explains how @final
marks run() as the fixed part of the Template
Method. Override run() in a subclass, run the
file, then run ty over it. Compare what each one
reports, and ask which of them reads the @final
marker. A # type: ignore on the override keeps the
listing in the build.
# The shape of exercise_3.py
from typing import final, override
class ApplicationFramework:
@final
def run(self) -> None:
...
def customize1(self) -> None: ...
def customize2(self) -> None: ...
class Reversed(ApplicationFramework):
@override
def run(self) -> None: # type: ignore
...
@override
def customize1(self) -> None:
...
@override
def customize2(self) -> None:
...# exercise_3.py
from typing import final, override
class ApplicationFramework:
@final
def run(self) -> None:
for _ in range(2):
self.customize1()
self.customize2()
def customize1(self) -> None: ...
def customize2(self) -> None: ...
class Reversed(ApplicationFramework):
@override
def run(self) -> None: # type: ignore
for _ in range(2):
self.customize2()
self.customize1()
@override
def customize1(self) -> None:
print("one")
@override
def customize2(self) -> None:
print("two")
Reversed().run()
#: two
#: one
#: two
#: onePython objects to nothing. The program runs, and the steps come out in the reversed order the subclass chose. The anchored algorithm is no longer anchored.
ty objects. The override carries a
# type: ignore so this listing stays in the book’s
build:
error[override-of-final-method]: Cannot override `ApplicationFramework.run`
info: `ApplicationFramework.run` is decorated with `@final`, forbidding overrides
The guarantee comes from the type checker, not the language.
@final sets __final__ = True on the
function, and nothing in the interpreter consults that
attribute. The Template Method’s central guarantee is
therefore in the same category as every other annotation in this
book: a tool enforces it before the program executes, and only
when you run that tool.
@final protects a codebase whose build runs a
type checker, and protects nothing in a codebase that does not.
When the interpreter must refuse the override, use the
__init_subclass__() check from the chapter’s near_miss.py, which reads
__final__ from each method a subclass replaces. The
check raises a TypeError at the subclass’s
class statement, as soon as the class body has run,
long before anyone constructs an instance.
Write two subclasses of
ApplicationFrameworkthat both type-check but break the anchored algorithm: one whosecustomize1()raises an exception the base does not raise, and one that leavescustomize2()at its...default when the flow depends on it. The type checker reports neither. What must be true of the base class for the type checker to catch either one?
Substitutability
shows a subclass that type-checks yet breaks the algorithm the
base class anchors. Write one subclass whose
customize1() raises an exception, and one that
keeps the ... default for a step on which the flow
depends. The base class can declare a step mandatory with
ABC and @abstractmethod. Consider
which of the two failures any declaration available in Python
could expose.
# The shape of exercise_4.py
from typing import final, override
from exceptions import expect
class ApplicationFramework:
@final
def run(self) -> None:
...
def customize1(self) -> None: ...
def customize2(self) -> None: ...
class Exploder(ApplicationFramework):
@override
def customize1(self) -> None:
...
class HalfDone(ApplicationFramework):
def __init__(self) -> None:
...
@override
def customize1(self) -> None:
...# exercise_4.py
from typing import final, override
from exceptions import expect
class ApplicationFramework:
@final
def run(self) -> None:
for _ in range(2):
self.customize1()
self.customize2()
def customize1(self) -> None: ...
def customize2(self) -> None: ...
class Exploder(ApplicationFramework):
@override
def customize1(self) -> None:
raise RuntimeError("step 1 refuses")
class HalfDone(ApplicationFramework):
def __init__(self) -> None:
self.pending: list[str] = []
@override
def customize1(self) -> None:
self.pending.append("work")
# The `...` default on customize2() drains nothing
expect(RuntimeError, Exploder().run)
#: [RuntimeError] step 1 refuses
app = HalfDone()
app.run()
print(app.pending)
#: ['work', 'work']The type checker reports nothing about either class. Both
override with the right name, the right parameters, and the
right return type, so both satisfy @override and
every signature rule the base class states.
Raise an unexpected exception.
Exploder breaks the algorithm on the first step of
the first pass. Code written against
ApplicationFramework expects run() to
return normally for every subclass the base contemplates.
Exploder raises an exception instead, so a caller
with no try around run() gets an
exception out of a method that advertised none.
Skip a step the flow needs.
HalfDone breaks the algorithm more quietly, which
makes it the worse of the two. customize1()
accumulates work for customize2() to consume, so
the pair is a two-step flow. Leaving customize2()
at its default breaks the second half, and the program neither
raises an exception nor prints anything wrong.
pending grows on every pass. Nothing shows from
outside until whatever pending feeds runs out of
memory or reports stale data.
Exploder and HalfDone need
different things from a type checker, and only one of those
things exists.
HalfDone’s omission is repairable. The
... body makes the step optional, and that is the
base class’s decision: it declares that a subclass may skip this
step. If the base class declares instead that a subclass may
not, by inheriting from ABC and marking
customize2() with @abstractmethod,
Python refuses to construct HalfDone. The type
checker reports the construction too, before the program runs.
No checker could catch the omission before, because
“deliberately empty” and “forgotten” were the same code, and
only the base class could have recorded that difference.
Exploder’s exception is not repairable this way.
For a type checker to catch Exploder, the base
class must state which exceptions a step may raise, and the
checker must hold every override to that list, the mechanism
Java’s throws clause provides. Python has no such
declaration, and no annotation expresses “this raises nothing.”
An exception type in a docstring is a note to a human. Only
discipline, review, or a test catches Exploder.
That split is the chapter’s point stated from the other side.
@final protects the shape of the algorithm, and
@abstractmethod protects the presence of a step,
because both are properties of the class structure that a base
class can declare. What a step does once called is behavior, and
Liskov substitution is a rule about behavior, so enforcing it
stays where the chapter leaves it: with you.
In
near_miss.py, subclassMyAppwith a class that adds a method namedreports(). Predict what theclassstatement does, then run it. Against which names does__init_subclass__()compare a new method? Change the check so it compares a new method only against the namesApplicationFrameworkdefines. What does the narrower check no longer catch?
Hooks
and the Misspelled Override builds the set of known names
inside __init_subclass__() from the classes in
cls.__mro__. Add a method to MyApp,
subclass it with a method name one letter off a known one, and
watch which names the check sees. For the narrower version, take
the names from vars(ApplicationFramework) alone.
Then test a misspelling of a name that a subclass added.
# The shape of exercise_5.py
from difflib import get_close_matches
from typing import final, override
from exceptions import expected
class ApplicationFramework:
@final
def run(self) -> None:
...
def customize1(self) -> None: ...
def customize2(self) -> None: ...
def __init_subclass__(cls) -> None:
...
class MyApp(ApplicationFramework):
@override
def customize1(self) -> None:
...
def report(self) -> None: ...# The shape of exercise_5_narrow.py
from difflib import get_close_matches
from typing import final, override
from exceptions import expected
class ApplicationFramework:
@final
def run(self) -> None:
...
def customize1(self) -> None: ...
def customize2(self) -> None: ...
def __init_subclass__(cls) -> None:
...
class MyApp(ApplicationFramework):
@override
def customize1(self) -> None:
...
def report(self) -> None: ...
class Audited(MyApp):
def reports(self) -> None: ...The chapter’s __init_subclass__() builds its set
of names from every base class, so the set grows as the
hierarchy does. MyApp adds report(),
and a subclass of MyApp inherits that name along
with the framework’s own:
# exercise_5.py
from difflib import get_close_matches
from typing import final, override
from exceptions import expected
class ApplicationFramework:
@final
def run(self) -> None:
for _ in range(2):
self.customize1()
self.customize2()
def customize1(self) -> None: ...
def customize2(self) -> None: ...
def __init_subclass__(cls) -> None:
super().__init_subclass__()
inherited = {
name
for base in cls.__mro__[1:]
for name in vars(base)
if not name.startswith("__")
}
for name in vars(cls):
if name.startswith("__"):
continue
replaced = getattr(super(cls, cls), name, None)
if getattr(replaced, "__final__", False):
raise TypeError(
f"{cls.__name__}.{name} "
"overrides a @final method"
)
if name in inherited:
continue
if near := get_close_matches(name, inherited):
raise TypeError(
f"{cls.__name__}.{name}: "
f"did you mean {near[0]}?"
)
class MyApp(ApplicationFramework):
@override
def customize1(self) -> None:
print("one")
def report(self) -> None: ...
with expected(TypeError):
class Audited(MyApp):
def reports(self) -> None: ...
#: [TypeError] Audited.reports: did you mean report?Compare against every base. The
class Audited statement raises a
TypeError. The check compares a new method against
every non-dunder name in every base, and report is
one of them, although report() is a helper that
MyApp added and no step of the framework. The
framework enforces a rule about a name it did not define.
The narrower check reads its names from
ApplicationFramework alone:
# exercise_5_narrow.py
from difflib import get_close_matches
from typing import final, override
from exceptions import expected
class ApplicationFramework:
@final
def run(self) -> None:
for _ in range(2):
self.customize1()
self.customize2()
def customize1(self) -> None: ...
def customize2(self) -> None: ...
def __init_subclass__(cls) -> None:
super().__init_subclass__()
declared = {
name
for name in vars(ApplicationFramework)
if not name.startswith("__")
}
for name in vars(cls):
if name.startswith("__"):
continue
replaced = getattr(super(cls, cls), name, None)
if getattr(replaced, "__final__", False):
raise TypeError(
f"{cls.__name__}.{name} "
"overrides a @final method"
)
if name in declared:
continue
if near := get_close_matches(name, declared):
raise TypeError(
f"{cls.__name__}.{name}: "
f"did you mean {near[0]}?"
)
class MyApp(ApplicationFramework):
@override
def customize1(self) -> None:
print("one")
def report(self) -> None: ...
class Audited(MyApp):
def reports(self) -> None: ...
print(Audited.reports.__qualname__)
#: Audited.reports
with expected(TypeError):
class Typo(MyApp):
def customise2(self) -> None: ...
#: [TypeError] Typo.customise2: did you mean customize2?Draw names from the framework alone.
ApplicationFramework exists by the time any
subclass’s class statement runs, so
__init_subclass__() can name it.
Pass the helper, catch the step.
Audited now finishes, and a misspelled step still
fails at any depth of the hierarchy, because the check compares
every subclass against the framework’s three names.
The narrower check no longer catches a misspelling of a name
that a subclass introduced. If Audited meant to
override report(), its reports() is a
new method that nothing calls, and the framework reports
nothing. Catching that misspelling falls to
Audited’s author, who can use the protection the
chapter recommends for steps: @override on the
method, and a type checker in the build.