Abstract Factory in Python: Multiple Product Families
Abstract Factory is a creational pattern that provides an interface for creating families of related objects without specifying their concrete classes. The key distinction from the simpler Factory...
Key Insights
- Abstract Factory creates entire families of related objects that work together, ensuring consistency across product variants without coupling client code to concrete implementations.
- Python’s ABC module and duck typing make Abstract Factory implementation cleaner than in statically-typed languages, but the pattern still adds significant complexity that must be justified.
- Use Abstract Factory when you have multiple product families that must remain internally consistent—not just when you want to hide object creation behind a factory method.
Introduction to Abstract Factory Pattern
Abstract Factory is a creational pattern that provides an interface for creating families of related objects without specifying their concrete classes. The key distinction from the simpler Factory Method pattern is scope: Factory Method creates one product type, while Abstract Factory creates entire product families that belong together.
Think of it this way: Factory Method answers “how do I create a button?” Abstract Factory answers “how do I create a complete, consistent set of UI components that all share the same visual language?”
The pattern involves four key participants: abstract products (interfaces for each product type), concrete products (implementations for each family), an abstract factory (interface declaring creation methods for all products), and concrete factories (implementations that produce products from one family).
The Problem: Cross-Platform UI Components
Consider building a desktop application that must support multiple design systems. Your application needs buttons, checkboxes, and dialogs—but these components must look and behave consistently within each theme. A Material Design button shouldn’t appear next to a Fluent Design checkbox.
Here’s the naive approach most developers start with:
def create_button(theme: str, label: str):
if theme == "material":
return MaterialButton(label)
elif theme == "fluent":
return FluentButton(label)
elif theme == "macos":
return MacOSButton(label)
else:
raise ValueError(f"Unknown theme: {theme}")
def create_checkbox(theme: str, label: str):
if theme == "material":
return MaterialCheckbox(label)
elif theme == "fluent":
return FluentCheckbox(label)
elif theme == "macos":
return MacOSCheckbox(label)
else:
raise ValueError(f"Unknown theme: {theme}")
def create_dialog(theme: str, title: str, message: str):
if theme == "material":
return MaterialDialog(title, message)
elif theme == "fluent":
return FluentDialog(title, message)
elif theme == "macos":
return MacOSDialog(title, message)
else:
raise ValueError(f"Unknown theme: {theme}")
This approach has serious problems. Every function duplicates the theme-switching logic. Adding a new theme requires modifying every factory function. There’s no guarantee that components created separately will be from the same family—you could accidentally mix themes. The theme string gets passed everywhere, creating tight coupling throughout your codebase.
Defining Abstract Products
Start by defining abstract base classes for each product type. These establish the contract that all concrete implementations must follow:
from abc import ABC, abstractmethod
class AbstractButton(ABC):
def __init__(self, label: str):
self.label = label
@abstractmethod
def render(self) -> str:
"""Return the rendered button markup."""
pass
@abstractmethod
def on_click(self, handler: callable) -> None:
"""Attach a click handler to the button."""
pass
class AbstractCheckbox(ABC):
def __init__(self, label: str, checked: bool = False):
self.label = label
self.checked = checked
@abstractmethod
def render(self) -> str:
"""Return the rendered checkbox markup."""
pass
@abstractmethod
def toggle(self) -> None:
"""Toggle the checkbox state."""
pass
class AbstractDialog(ABC):
def __init__(self, title: str, message: str):
self.title = title
self.message = message
@abstractmethod
def render(self) -> str:
"""Return the rendered dialog markup."""
pass
@abstractmethod
def show(self) -> None:
"""Display the dialog to the user."""
pass
@abstractmethod
def close(self) -> None:
"""Close the dialog."""
pass
Notice that abstract products can contain shared implementation in __init__. The @abstractmethod decorator ensures subclasses implement the required behavior while allowing common state management in the base class.
Implementing Concrete Product Families
Now implement two complete product families. Each family’s components share visual and behavioral characteristics:
class MaterialButton(AbstractButton):
def render(self) -> str:
return f'<button class="mdc-button mdc-button--raised">{self.label}</button>'
def on_click(self, handler: callable) -> None:
# Material ripple effect before handler
print(f"[Material] Ripple effect on '{self.label}'")
handler()
class MaterialCheckbox(AbstractCheckbox):
def render(self) -> str:
checked_attr = 'checked' if self.checked else ''
return f'''<div class="mdc-checkbox">
<input type="checkbox" class="mdc-checkbox__native-control" {checked_attr}/>
<label>{self.label}</label>
</div>'''
def toggle(self) -> None:
self.checked = not self.checked
print(f"[Material] Checkbox '{self.label}' -> {self.checked}")
class MaterialDialog(AbstractDialog):
def render(self) -> str:
return f'''<div class="mdc-dialog">
<div class="mdc-dialog__surface">
<h2 class="mdc-dialog__title">{self.title}</h2>
<div class="mdc-dialog__content">{self.message}</div>
</div>
</div>'''
def show(self) -> None:
print(f"[Material] Opening dialog with slide-up animation: {self.title}")
def close(self) -> None:
print(f"[Material] Closing dialog with fade-out: {self.title}")
class FluentButton(AbstractButton):
def render(self) -> str:
return f'<button class="ms-Button ms-Button--primary">{self.label}</button>'
def on_click(self, handler: callable) -> None:
# Fluent uses different feedback
print(f"[Fluent] Highlight effect on '{self.label}'")
handler()
class FluentCheckbox(AbstractCheckbox):
def render(self) -> str:
checked_attr = 'checked' if self.checked else ''
return f'''<div class="ms-Checkbox">
<input type="checkbox" class="ms-Checkbox-input" {checked_attr}/>
<span class="ms-Checkbox-label">{self.label}</span>
</div>'''
def toggle(self) -> None:
self.checked = not self.checked
print(f"[Fluent] Checkbox '{self.label}' toggled to {self.checked}")
class FluentDialog(AbstractDialog):
def render(self) -> str:
return f'''<div class="ms-Dialog">
<div class="ms-Dialog-main">
<div class="ms-Dialog-title">{self.title}</div>
<div class="ms-Dialog-content">{self.message}</div>
</div>
</div>'''
def show(self) -> None:
print(f"[Fluent] Opening dialog with scale animation: {self.title}")
def close(self) -> None:
print(f"[Fluent] Closing dialog with slide-down: {self.title}")
Each family implements the same interfaces but with family-specific markup, animations, and behaviors. The Material components use Google’s Material Design classes and ripple effects. The Fluent components use Microsoft’s Fluent UI patterns with different animations.
The Abstract Factory Interface and Concrete Factories
Now define the factory interface and concrete implementations:
class UIFactory(ABC):
@abstractmethod
def create_button(self, label: str) -> AbstractButton:
pass
@abstractmethod
def create_checkbox(self, label: str, checked: bool = False) -> AbstractCheckbox:
pass
@abstractmethod
def create_dialog(self, title: str, message: str) -> AbstractDialog:
pass
class MaterialFactory(UIFactory):
def create_button(self, label: str) -> AbstractButton:
return MaterialButton(label)
def create_checkbox(self, label: str, checked: bool = False) -> AbstractCheckbox:
return MaterialCheckbox(label, checked)
def create_dialog(self, title: str, message: str) -> AbstractDialog:
return MaterialDialog(title, message)
class FluentFactory(UIFactory):
def create_button(self, label: str) -> AbstractButton:
return FluentButton(label)
def create_checkbox(self, label: str, checked: bool = False) -> AbstractCheckbox:
return FluentCheckbox(label, checked)
def create_dialog(self, title: str, message: str) -> AbstractDialog:
return FluentDialog(title, message)
Each concrete factory guarantees that all products it creates belong to the same family. You cannot accidentally get a Material button from a Fluent factory.
Client Code and Dependency Injection
The real power emerges in client code. The application works with factories and products through their abstract interfaces:
class Application:
def __init__(self, factory: UIFactory):
self.factory = factory
self._components: list = []
def create_login_form(self) -> None:
submit_btn = self.factory.create_button("Sign In")
remember_me = self.factory.create_checkbox("Remember me", checked=False)
self._components.extend([submit_btn, remember_me])
print("Login form created:")
print(submit_btn.render())
print(remember_me.render())
def show_error(self, message: str) -> None:
dialog = self.factory.create_dialog("Error", message)
dialog.show()
print(dialog.render())
# Factory registry for configuration-based selection
FACTORY_REGISTRY: dict[str, type[UIFactory]] = {
"material": MaterialFactory,
"fluent": FluentFactory,
}
def get_factory(theme: str) -> UIFactory:
factory_class = FACTORY_REGISTRY.get(theme.lower())
if factory_class is None:
available = ", ".join(FACTORY_REGISTRY.keys())
raise ValueError(f"Unknown theme '{theme}'. Available: {available}")
return factory_class()
# Usage
if __name__ == "__main__":
import os
# Theme from environment or config
theme = os.environ.get("APP_THEME", "material")
factory = get_factory(theme)
app = Application(factory)
app.create_login_form()
app.show_error("Invalid credentials")
The Application class has no knowledge of Material or Fluent specifics. Swapping themes requires only changing the factory passed to the constructor. The registry pattern makes adding new themes straightforward—implement the products and factory, add one line to the registry.
Trade-offs and When to Use
Abstract Factory adds significant complexity. You’re creating parallel class hierarchies: one for each product type, one for each family, plus the factories. For our example, that’s 9 classes (3 abstract products, 6 concrete products) plus 3 factories (1 abstract, 2 concrete). This is substantial overhead.
Use Abstract Factory when:
- You have multiple families of related products that must be used together
- Products within a family have coordinated behavior or appearance
- You need to enforce consistency—mixing products from different families would be a bug
- You expect to add new product families (new themes) more often than new product types
Avoid Abstract Factory when:
- You only have one product type (use Factory Method instead)
- Products don’t need to be consistent with each other
- You have only one or two families and don’t expect more
- The relationships between products are loose or nonexistent
The pattern aligns well with SOLID principles. It follows Open/Closed—add new families without modifying existing code. It enforces Dependency Inversion—high-level modules depend on abstractions. It supports Single Responsibility—each factory handles one family.
In Python specifically, you can often simplify with duck typing. If your products don’t need inheritance hierarchies, you might skip the abstract product classes entirely and rely on protocols or informal interfaces. But for complex domains where type safety and explicit contracts matter, the full pattern provides valuable guarantees.
Abstract Factory shines in plugin architectures, theming systems, cross-platform development, and anywhere you need interchangeable families of cooperating objects. Use it deliberately, understanding that you’re trading simplicity for flexibility and consistency guarantees.