Implementing Template Pattern using Python


At its core, the Template Method pattern has two parts:
An abstract base class (or interface) that defines a “template method”: a public method that outlines the sequence of steps needed to complete an operation. Some of those steps are implemented in the base class, while others are marked as abstract (or left unimplemented) so that subclasses must override them.
Concrete subclasses that provide the missing pieces. Each subclass fills in the abstract methods with its own domain-specific behavior.
By following this pattern, you:
Enforce a consistent process. The base class guarantees that every notification pipeline will:
authenticate
build a message
send that message
log what was sent
Encapsulate variation. Each subclass only needs to implement the parts that vary (e.g., “_authenticate” and “_send”), while the base class takes care of the rest.
Promote code reuse. Common functionality—such as building a message string or writing a log entry—lives in one place (the abstract base), so you don’t duplicate it across multiple subclasses.
Below is the complete code for our example, taken from the original snippet:
import datetime
import inspect
from abc import ABC, abstractmethod
class NotificationPipeline(ABC):
@abstractmethod
def _authenticate(self) -> None:
...
@abstractmethod
def _send(self, message: str) -> None:
...
def _build_message(self) -> str:
return f"{self.subject}: {self.body}"
def _log(self, message: str) -> None:
print(f"[{datetime.datetime.now()}] {message}")
def execute(self):
"""Template method: authenticate, build, send, and log a notification."""
self._authenticate()
msg = self._build_message()
self._send(msg)
self._log(msg)
class EmailNotificationPipeline(NotificationPipeline):
def __init__(self, recipient: str, subject: str, body: str):
self.recipient = recipient
self.subject = subject
self.body = body
def _authenticate(self) -> None:
print(f"Authenticating inside {__class__.__name__} and {inspect.currentframe().f_code.co_name}")
def _send(self, message: str) -> None:
print(f"Email to {self.recipient} sent: {message}")
email_pipeline = EmailNotificationPipeline(
recipient="alice@example.com",
subject="Weekly Report",
body="Your report is ready."
)
email_pipeline.execute()
Let’s break down what’s happening here.
Breaking Down the Abstract Base Class
class NotificationPipeline(ABC):
By inheriting from ABC
, we declare that NotificationPipeline
is an abstract base class. We never intend to instantiate it directly. Instead, it lays out the structure for any subclass.
Abstract Methods: _authenticate
and _send
@abstractmethod
def _authenticate(self) -> None:
...
@abstractmethod
def _send(self, message: str) -> None:
...
These two methods are marked with @abstractmethod
. That means:
Subclasses must override them. If a subclass does not provide its own implementation of both
_authenticate
and_send
, Python will refuse to let you instantiate that subclass.The base class itself does not know how to authenticate or how to send. It just knows that every pipeline needs those two steps.
Shared Implementation: _build_message
and _log
def _build_message(self) -> str:
return f"{self.subject}: {self.body}"
def _log(self, message: str) -> None:
print(f"[{datetime.datetime.now()}] {message}")
Here, the base class handles tasks that are common across all notification channels:
_build_message
concatenates the subject and body into a single string. It assumes thatself.subject
andself.body
exist on the instance; it does not care how they were set. Every subclass—or whoever instantiates a subclass—needs to ensure those attributes are populated._log
simply writes a timestamped print statement. You could easily replace this with a more sophisticated logger, write to a file, or send logs to an external monitoring service. But conceptually, every pipeline wants to log what just happened.
The Template Method Itself: execute
def execute(self):
"""Template method: authenticate, build, send, and log a notification."""
self._authenticate()
msg = self._build_message()
self._send(msg)
self._log(msg)
This is the heart of the Template Method pattern:
Call
_authenticate()
– this step must be implemented by a subclass.Build the message via
_build_message()
, which is a concrete (non-abstract) method in the base class.Call
_send(msg)
– again, implemented by a subclass.Log via
_log(msg)
.
Because execute()
is not marked @abstractmethod
, subclasses automatically inherit the same “workflow.” They cannot override the order of steps unless they explicitly override execute
, which defeats the purpose of the pattern. By convention, you give subclasses hooks for the steps that vary, but not for the high-level sequence itself.
A Concrete Example: Sending Email
class EmailNotificationPipeline(NotificationPipeline):
def __init__(self, recipient: str, subject: str, body: str):
self.recipient = recipient
self.subject = subject
self.body = body
def _authenticate(self) -> None:
print(f"Authenticating inside {__class__.__name__} and {inspect.currentframe().f_code.co_name}")
def _send(self, message: str) -> None:
print(f"Email to {self.recipient} sent: {message}")
Constructor Sets Up State
recipient
,subject
, andbody
are instance attributes that the base class’s_build_message
method will use. It’s the subclass’s responsibility to store any data needed for those shared steps.
Overriding _authenticate
Here we just
print()
a placeholder “Authenticating…” message. In a real system you might:Look up API credentials
Make a call to an SMTP server with TLS
Throw an exception if authentication fails
Notice how we use Python’s
inspect.currentframe().f_
code.co
_name
to dynamically print the method name. That can be handy for debugging, though in production you’d likely rely on a proper logger.
Overriding _send
- Again, this is a stub that prints “Email to … sent,” but you could imagine hooking into
smtplib
or an external email library to perform a real send.
Because both abstract methods are implemented, we can now instantiate and run:
email_pipeline = EmailNotificationPipeline(
recipient="alice@example.com",
subject="Weekly Report",
body="Your report is ready."
)
email_pipeline.execute()
When execute()
is called, the output is:
Authenticating inside EmailNotificationPipeline and _authenticate
Email to alice@example.com sent: Weekly Report: Your report is ready.
[2025-05-31 13:45:22.123456] Weekly Report: Your report is ready.
(Your actual timestamp will vary. The important thing is that each step executes in order.)
Why Use Template Method?
Consistency Across Variants
If you later createSMSNotificationPipeline
,PushNotificationPipeline
, orSlackNotificationPipeline
, each of them will follow exactly the same lifecycle: authenticate, build message, send, and log. You never forget to log or forget to build the message in the correct format.Single Place to Update Common Logic
Want to change how messages are built (e.g., prepend a timestamp or wrap them in JSON)? Update_build_message
in the base class once, and every subclass automatically benefits.Enforced Order of Operations
Becauseexecute()
lives in the abstract base class and is not meant to be overridden lightly, you guarantee that nobody accidentally sends a message without authenticating, or tries to log before building the message.Easier Testing
You can build unit tests against the base class (e.g., pass in dummy subclasses) to verify thatexecute()
calls each step in the right order. You can also individually test each subclass’s_authenticate
and_send
without worrying about the overall control flow.
Extending the Example
Imagine you now want to support “bulk email,” where you have a list of recipients and maybe a slightly different build process (for example, a personalized “Hello, Alice!” greeting). You could create:
class BulkEmailNotificationPipeline(NotificationPipeline):
def __init__(self, recipients: list[str], subject: str, body: str):
self.recipients = recipients
self.subject = subject
self.body = body
def _authenticate(self) -> None:
# Same or similar to single-email auth
print("Bulk email authentication...")
def _send(self, message: str) -> None:
for r in self.recipients:
personalized = f"Hi {r},\n{message}"
print(f"Email to {r} sent: {personalized}")
Because _build_message
and _log
already live in the base class, you don’t have to duplicate them. You simply customize the steps that actually send to multiple addresses.
Common Pitfalls & When to Use (or Not Use) Template Method
Pitfall: Too Many Hooks
If your base class has a dozen “protected” abstract methods that subclasses must override, the pattern becomes burdensome. Keep your template method focused—only abstract away the real points of variation.Pitfall: Overriding the Template Itself
Subclasses should rarely, if ever, override theexecute()
method. If many subclasses keep replacing the parent’s workflow, you might not be gaining much. Ideally, the base class handles orchestration, while subclasses fill in details.When It Fits
You have several classes that mostly do the same sequence of steps, but differ in just a few.
You want to guarantee that every variant follows the same high-level process in the same order.
You need to centralize logging, error handling, or pre/post conditions for the entire operation.
When to Consider Other Patterns
If different variants don’t share much in common, it might make more sense to separate them entirely.
If the order of steps changes dynamically at runtime (for example, based on a configuration file), you may need something more flexible like the Strategy pattern—where you treat each step as a “pluggable” component.
Final Thoughts
Design patterns like Template Pattern shine when you have a clear, repeatable workflow and want to isolate the common scaffolding from the variable implementation details. In our simple notification pipeline:
The abstract base class handles:
The high-level choreography (
execute()
)Shared helpers (
_build_message()
and_log()
)
Subclasses only worry about:
Authenticating in their own way (API key vs. username/password vs. OAuth)
Sending the message via the appropriate channel (email, SMS, Slack, push notifications, etc.)
As your notification requirements grow—perhaps adding retries, queuing, or more sophisticated logging—you can extend the base class without touching each individual subclass.
The next time you find yourself writing similar workflows over and over (authenticate → compute/build payload → send → log), pause and consider: “Could this be a Template Method?” Chances are you’ll end up with leaner, more maintainable code, and your future self will thank you.
Subscribe to my newsletter
Read articles from Chandrasekar(Chan) Rajaram directly inside your inbox. Subscribe to the newsletter, and don't miss out.
Written by
