Overview

This implementation shows the Template Method pattern with abstract base classes and inheritance. Python's abc module has built-in support for abstract methods, so the traditional approach stays simple and idiomatic.

Benefits of this approach

  • Clear algorithm structure. The abstract methods state exactly what subclasses must implement.
  • Explicit contracts. The base class makes the algorithm skeleton visible.
  • Type checking tools. IDEs and type checkers can verify implementations.
  • Familiar OOP pattern. It works like the traditional Template Method pattern.
  • Uses built-ins. The pattern relies on Python's abc module.

Traditional approach: inheritance with abstract methods

Base class with abstract methods

from abc import ABC, abstractmethod

class DataProcessorBase(ABC):
    """
    Abstract base class defining the Template Method pattern.

    The execute() method is the template method that defines the algorithm skeleton.
    Subclasses must implement the customizable steps.
    """

    def execute(self, data: str) -> None:
        """
        Template method: defines the algorithm skeleton.

        Steps are defined but subclasses customize them:
        1. Validate input
        2. Process data
        3. Save results
        """
        print("Starting data processing pipeline...")

        # Step 1: Validate
        validated = self.validate(data)
        if not validated:
            print("Validation failed. Aborting.")
            return

        # Step 2: Process
        processed = self.process(validated)
        print("Processing complete.")

        # Step 3: Save
        self.save(processed)

        print("Pipeline finished successfully.\n")

    # Abstract methods: subclasses must implement these
    @abstractmethod
    def validate(self, data: str) -> str:
        """Validate input. Return empty string if invalid."""
        pass

    @abstractmethod
    def process(self, data: str) -> str:
        """Process the validated data."""
        pass

    @abstractmethod
    def save(self, data: str) -> None:
        """Save the processed data."""
        pass

Concrete implementations

class CSVProcessor(DataProcessorBase):
    """Implementation for CSV data processing."""

    def validate(self, data: str) -> str:
        """Validate CSV format."""
        print("  [CSV Validate] Checking CSV format...")
        return data if data else ""

    def process(self, data: str) -> str:
        """Parse and clean CSV."""
        print("  [CSV Process] Parsing and cleaning CSV...")
        return f"{data} (cleaned)"

    def save(self, data: str) -> None:
        """Write to CSV file."""
        print(f"  [CSV Save] Writing to CSV file: {data}")


class JSONProcessor(DataProcessorBase):
    """Implementation for JSON data processing."""

    def validate(self, data: str) -> str:
        """Validate JSON syntax."""
        print("  [JSON Validate] Validating JSON syntax...")
        if not data or data[0] != '{':
            return ""
        return data

    def process(self, data: str) -> str:
        """Parse and format JSON."""
        print("  [JSON Process] Parsing and formatting JSON...")
        return f"{data} (formatted)"

    def save(self, data: str) -> None:
        """Write to JSON file."""
        print(f"  [JSON Save] Writing to JSON file: {data}")

Traditional usage example

def main():
    print("=== Template Method Pattern (Inheritance) ===\n")

    # CSV Pipeline
    print("1. CSV Pipeline:")
    csv_processor = CSVProcessor()
    csv_processor.execute("name,age,email")

    # JSON Pipeline
    print("2. JSON Pipeline:")
    json_processor = JSONProcessor()
    json_processor.execute('{"key": "value"}')

if __name__ == "__main__":
    main()

Output

=== Template Method Pattern (Inheritance) ===

1. CSV Pipeline:
Starting data processing pipeline...
  [CSV Validate] Checking CSV format...
  [CSV Process] Parsing and cleaning CSV...
Processing complete.
  [CSV Save] Writing to CSV file: name,age,email (cleaned)
Pipeline finished successfully.

2. JSON Pipeline:
Starting data processing pipeline...
  [JSON Validate] Validating JSON syntax...
  [JSON Process] Parsing and formatting JSON...
Processing complete.
  [JSON Save] Writing to JSON file: {"key": "value"} (formatted)
Pipeline finished successfully.

Key points: inheritance approach

Abstract base class

The DataProcessorBase class uses ABC and @abstractmethod to require implementations. Python refuses to instantiate abstract classes.

Template method

The execute() method is the template method that runs all the steps. Subclasses cannot override it, so the algorithm structure stays consistent.

Abstract methods

The protected methods (validate(), process(), save()) are declared abstract. Subclasses must implement all of them, or they remain abstract too.

Explicit contracts

The base class makes it clear what subclasses must do. Tools such as mypy can verify implementations.

Modern approach: callables (composition)

Python's first-class functions allow a flexible alternative: callables passed through the constructor. This approach avoids inheritance and keeps the same algorithm structure.

The algorithm class with callables

from typing import Callable

class DataProcessor:
    """
    Template method pattern using callable functions.

    Customizable steps are passed in the constructor,
    allowing flexible algorithm implementations without inheritance.
    """

    def __init__(
        self,
        validate: Callable[[str], str],
        process: Callable[[str], str],
        save: Callable[[str], None]
    ):
        """
        Initialize with custom algorithm steps.

        Args:
            validate: Function that validates input, returns empty string if invalid
            process: Function that processes the input
            save: Function that saves the processed data
        """
        self._validate = validate
        self._process = process
        self._save = save

    def execute(self, data: str) -> None:
        """
        Template method: defines the algorithm skeleton.

        Steps are customizable but the overall flow is fixed:
        1. Validate input
        2. Process data
        3. Save results
        """
        print("Starting data processing pipeline...")

        # Step 1: Validate
        validated = self._validate(data)
        if not validated:
            print("Validation failed. Aborting.")
            return

        # Step 2: Process
        processed = self._process(validated)
        print("Processing complete.")

        # Step 3: Save
        self._save(processed)

        print("Pipeline finished successfully.\n")

Function-based implementations

class CSVProcessorFn:
    """Implementation for CSV data processing (function-based)."""

    @staticmethod
    def validate(data: str) -> str:
        """Validate CSV format."""
        print("  [CSV Validate] Checking CSV format...")
        return data if data else ""

    @staticmethod
    def process(data: str) -> str:
        """Parse and clean CSV."""
        print("  [CSV Process] Parsing and cleaning CSV...")
        return f"{data} (cleaned)"

    @staticmethod
    def save(data: str) -> None:
        """Write to CSV file."""
        print(f"  [CSV Save] Writing to CSV file: {data}")


class JSONProcessorFn:
    """Implementation for JSON data processing (function-based)."""

    @staticmethod
    def validate(data: str) -> str:
        """Validate JSON syntax."""
        print("  [JSON Validate] Validating JSON syntax...")
        if not data or data[0] != '{':
            return ""
        return data

    @staticmethod
    def process(data: str) -> str:
        """Parse and format JSON."""
        print("  [JSON Process] Parsing and formatting JSON...")
        return f"{data} (formatted)"

    @staticmethod
    def save(data: str) -> None:
        """Write to JSON file."""
        print(f"  [JSON Save] Writing to JSON file: {data}")

Using lambda functions

class CustomProcessor:
    """Create processors with lambda functions."""

    @staticmethod
    def create_with_lambdas() -> DataProcessor:
        """Build processor using inline lambda expressions."""
        return DataProcessor(
            # Validate: reject if contains special chars
            validate=lambda data: (
                print("  [Lambda Validate] Checking for special characters...") or
                ("" if "!" in data else data)
            ),
            # Process: convert to uppercase
            process=lambda data: (
                print("  [Lambda Process] Converting to uppercase...") or
                data.upper()
            ),
            # Save: print to console
            save=lambda data: print(f"  [Lambda Save] Output: {data}")
        )

Using instance methods as callbacks

class EmailProcessor:
    """Process and validate email data."""

    def __init__(self, domain_whitelist: list):
        self.domain_whitelist = domain_whitelist

    def validate(self, data: str) -> str:
        """Validate email format and domain."""
        print("  [Email Validate] Checking email format...")
        if "@" not in data:
            return ""
        domain = data.split("@")[1]
        return data if domain in self.domain_whitelist else ""

    def process(self, data: str) -> str:
        """Clean and normalize email."""
        print("  [Email Process] Normalizing email...")
        return data.lower().strip()

    def save(self, data: str) -> None:
        """Write to email database."""
        print(f"  [Email Save] Storing in database: {data}")

Composition usage example

def main():
    print("=== Template Method Pattern (Callables) ===\n")

    # CSV Pipeline
    print("1. CSV Pipeline:")
    csv_pipeline = DataProcessor(
        CSVProcessorFn.validate,
        CSVProcessorFn.process,
        CSVProcessorFn.save
    )
    csv_pipeline.execute("name,age,email")

    # JSON Pipeline
    print("2. JSON Pipeline:")
    json_pipeline = DataProcessor(
        JSONProcessorFn.validate,
        JSONProcessorFn.process,
        JSONProcessorFn.save
    )
    json_pipeline.execute('{"key": "value"}')

    # Lambda-based Pipeline
    print("3. Lambda-based Pipeline:")
    custom_pipeline = CustomProcessor.create_with_lambdas()
    custom_pipeline.execute("hello")

    # Failed Validation Example
    print("4. Failed Validation Example:")
    custom_pipeline.execute("invalid!")

    # Email Processor with Instance Methods
    print("5. Email Processor:")
    email_processor = EmailProcessor(
        domain_whitelist=["example.com", "company.com"]
    )
    email_pipeline = DataProcessor(
        email_processor.validate,
        email_processor.process,
        email_processor.save
    )
    email_pipeline.execute("User@Example.com")
    email_pipeline.execute("user@invalid.com")

if __name__ == "__main__":
    main()

Output (callables)

=== Template Method Pattern (Callables) ===

1. CSV Pipeline:
Starting data processing pipeline...
  [CSV Validate] Checking CSV format...
  [CSV Process] Parsing and cleaning CSV...
Processing complete.
  [CSV Save] Writing to CSV file: name,age,email (cleaned)
Pipeline finished successfully.

2. JSON Pipeline:
Starting data processing pipeline...
  [JSON Validate] Validating JSON syntax...
  [JSON Process] Parsing and formatting JSON...
Processing complete.
  [JSON Save] Writing to JSON file: {"key": "value"} (formatted)
Pipeline finished successfully.

3. Lambda-based Pipeline:
Starting data processing pipeline...
  [Lambda Validate] Checking for special characters...
  [Lambda Process] Converting to uppercase...
Processing complete.
  [Lambda Save] Output: HELLO
Pipeline finished successfully.

4. Failed Validation Example:
Starting data processing pipeline...
  [Lambda Validate] Checking for special characters...
Validation failed. Aborting.

5. Email Processor:
Starting data processing pipeline...
  [Email Validate] Checking email format...
  [Email Process] Normalizing email...
Processing complete.
  [Email Save] Storing in database: user@example.com
Pipeline finished successfully.

Starting data processing pipeline...
  [Email Validate] Checking email format...
Validation failed. Aborting.

Key points: callables approach

Constructor dependency injection

The DataProcessor constructor accepts all customizable steps up front:

DataProcessor(validate, process, save)

This makes dependencies explicit and lets you pass mock functions in tests.

Callable types in Python

Any of these work as callables:

  • Functions: def my_func(data): ...
  • Lambdas: lambda data: ...
  • Methods: obj.method
  • Static methods: ClassName.method
  • Callables: any object with a __call__ method

Duck typing

Python does not require explicit interface implementation. Any object that is callable with the right signature works:

# Even a custom callable class works
class MyValidator:
    def __call__(self, data: str) -> str:
        return data if data else ""

processor = DataProcessor(
    MyValidator(),
    process_func,
    save_func
)

Runtime flexibility

Unlike inheritance, you can change behavior at runtime by passing different functions or modifying closures.

Advanced example: decorating behavior

def with_logging(name: str, fn: Callable) -> Callable:
    """Decorator to add logging to any algorithm step."""
    def wrapper(data: str):
        print(f"[{name}] Starting...")
        result = fn(data)
        print(f"[{name}] Complete.")
        return result
    return wrapper

# Use decorator to wrap steps
logged_pipeline = DataProcessor(
    with_logging("Validate", CSVProcessorFn.validate),
    with_logging("Process", CSVProcessorFn.process),
    with_logging("Save", CSVProcessorFn.save)
)

Comparison: inheritance vs callables

Aspect Inheritance Callables (Composition)
Structure Class hierarchy Simple composition
Coupling Tight, hierarchical Loose, flexible
Inheritance Required Not required
Testing Requires mock subclasses Easy with mock functions
Runtime changes No Yes, trivial
Complexity Class hierarchies Simple function composition
Pythonic Uses the abc module Uses first-class functions
Type checking Explicit type hierarchy Type hints with Callable
Boilerplate Less (uses abc) Minimal
Extensibility Through subclassing Through function composition

When to use

Inheritance approach:

  • Traditional OOP design fits the codebase
  • You want the abc module to enforce explicit contracts
  • Tight coupling is acceptable for the domain model
  • Subclasses need their own state and behavior
  • The team knows inheritance patterns

Callables approach:

  • You need runtime flexibility
  • You have a plugin or strategy-like architecture
  • You want to avoid inheritance hierarchies
  • Testing with mock functions is a priority
  • You prefer functional programming style

Summary

Inheritance makes the contract explicit and lets the base class enforce the flow. Callables keep the same flow with less structure and more freedom. Use inheritance when the step implementations belong to a type. Use callables when the caller assembles the algorithm from functions it already has.