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