Implementing Template Pattern using Python

At its core, the Template Method pattern has two parts:

  1. 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.

  2. 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:

    1. authenticate

    2. build a message

    3. send that message

    4. 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 that self.subject and self.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:

  1. Call _authenticate() – this step must be implemented by a subclass.

  2. Build the message via _build_message(), which is a concrete (non-abstract) method in the base class.

  3. Call _send(msg) – again, implemented by a subclass.

  4. 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, and body 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?

  1. Consistency Across Variants
    If you later create SMSNotificationPipeline, PushNotificationPipeline, or SlackNotificationPipeline, 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.

  2. 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.

  3. Enforced Order of Operations
    Because execute() 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.

  4. Easier Testing
    You can build unit tests against the base class (e.g., pass in dummy subclasses) to verify that execute() 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 the execute() 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:

  1. The abstract base class handles:

    • The high-level choreography (execute())

    • Shared helpers (_build_message() and _log())

  2. 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.

0
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

Chandrasekar(Chan) Rajaram
Chandrasekar(Chan) Rajaram