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 step the subclass or the function supplies, and writes the output to the last file. Supply each of these steps 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 shared 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
input’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(). 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'The two mechanisms supply the same step, so both produce
'HELLO\nWORLD\n'.
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 passes 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 they store the word list. Search stores it
in a field. The lambda closes over the local variable
words. In both, run() and
run_file_framework() stay the same. A new policy is
a new step. The algorithm that calls the step belongs to the
framework.
Before you move on, name what separates your attempt from this one: a different design, a detail, or an equivalent alternative.
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") completes and prints nothing,
because the engine starts in Framework.__init__(),
which Greeter now bypasses. The type checker
accepts the class too. The reorder keeps the call and moves it
after the setup. The redesign takes the start out of
construction and gives it to the client.
The quick repair reorders the two lines so the subclass
finishes its own setup before it calls
Framework.__init__():
# 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 drops its __init__(), so the
client builds a finished object and then 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 protects the next author. The reorder protects
the one subclass that performs it. Each later subclass is safe
only if its author remembers to do the same. The reorder asks
every future subclass author to invert the convention they have
used everywhere else, which is to call
super().__init__() first. The signature of
Framework.__init__() is silent on the order. Every
type checker accepts the usual one. Python reports the failure
as an AttributeError raised in the subclass’s own
step(), called from a base class someone else
wrote. The reorder turns safety into a rule the next author must
remember. That author may not have read this chapter.
Separating construction from starting makes the mistake
impossible, because the engine starts in run(), and
the client calls run() on a finished object. Each
call site gains a line, greeter.run(). That line
moves the decision about when the algorithm starts from the base
class to the client. The same reasoning favors eager creation in
Singleton.
Eager creation builds the object at import time, before any
thread can read a half-built one.
Before you move on, name what separates your attempt from this one: a different design, a detail, or an equivalent alternative.
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?
The
Anchored Algorithm explains how @final marks
run() as the part of the Template Method
that stays the same. Override run() in a subclass,
run the file, then run ty over it. Compare what
Python and ty each report, 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 prints the steps in the reversed order the subclass chose. The anchored algorithm is no longer anchored.
ty objects. The override carries a
# type: ignore to keep this listing in the book’s
build. With that comment removed, ty reports:
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. 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.
Before you move on, name what separates your attempt from this one: a different design, a detail, or an equivalent alternative.
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 anchored algorithm 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 accepts both classes. 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. Exploder’s
customize1() raises an exception instead, which
propagates out of run() into a caller written for
the base version.
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. The program still
completes normally. pending grows on every pass.
The flaw stays hidden until the program runs out of memory or
whatever reads pending reports stale data.
Exploder and HalfDone need
different things from a type checker. Only one of those things
exists.
HalfDone’s omission is repairable. Through the
... body, the base class declares that a subclass
may skip this step. With that body, no checker can catch the
omission, because “deliberately empty” and “forgotten” are the
same code. Only the base class can record that difference. The
base class can declare instead that a subclass must supply the
step, by inheriting from ABC and marking
customize2() with @abstractmethod.
Then constructing HalfDone raises a
TypeError. The type checker reports the
construction before the program runs.
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 compare every override with that list. Java’s
throws clause provides that mechanism for checked
exceptions. 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. No tool checks
substitution, so the chapter leaves that check to your
discipline.
Before you move on, name what separates your attempt from this one: a different design, a detail, or an equivalent alternative.
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__. MyApp defines
report(). Subclass it, add a method whose name is
one letter off a known name, 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: ...
class CheckedFramework(ApplicationFramework):
def __init_subclass__(cls) -> None:
...
class MyApp(CheckedFramework):
@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: ...
class CheckedFramework(ApplicationFramework):
def __init_subclass__(cls) -> None:
...
class MyApp(CheckedFramework):
@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(). A
subclass of MyApp inherits that name along with the
framework’s own names:
# 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: ...
class CheckedFramework(ApplicationFramework):
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(CheckedFramework):
@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 those names. Yet report() is a helper that
MyApp added, not a step of the framework, so the
framework enforces a rule about a name that belongs to
MyApp.
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: ...
class CheckedFramework(ApplicationFramework):
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(CheckedFramework):
@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. The
class Audited statement now completes. A misspelled
step still fails at any depth of the hierarchy, because the
check compares every subclass against the framework’s three
names.
A misspelling of a name that a subclass introduced now passes
the narrower check. If Audited meant to override
report(), its reports() is a new
method that nothing calls. The framework accepts it.
Audited’s author must catch that misspelling, using
the protection the chapter recommends for steps:
@override on the method, and a type checker in the
build.
Before you move on, name what separates your attempt from this one: a different design, a detail, or an equivalent alternative.