Creational Design Patterns
Creational patterns deal with object creation — they abstract instantiation so your code depends on interfaces, not concrete classes. All examples below are Python-first, complete, and runnable.
Singleton Pattern
Purpose: Guarantee exactly one instance of a class and expose a single global access point.
When to use:
- Shared resources: logging, configuration, connection pools, caches
- You need lazy initialization (create on first use)
- Multiple components must coordinate through one object
| from __future__ import annotations
import threading
from datetime import datetime
class Logger:
_instance: Logger | None = None
_lock = threading.Lock()
def __new__(cls) -> Logger:
if cls._instance is None:
with cls._lock:
if cls._instance is None:
cls._instance = super().__new__(cls)
cls._instance._initialized = False
return cls._instance
def __init__(self) -> None:
if self._initialized:
return
self._initialized = True
def info(self, message: str) -> None:
self._log("INFO", message)
def warn(self, message: str) -> None:
self._log("WARN", message)
def error(self, message: str) -> None:
self._log("ERROR", message)
def _log(self, level: str, message: str) -> None:
timestamp = datetime.now().strftime("%Y-%m-%d %H:%M:%S")
print(f"{timestamp} [{level}]: {message}")
if __name__ == "__main__":
a = Logger()
b = Logger()
print(a is b) # True — same instance
a.info("Application started")
b.warn("Cache warming")
a.error("Disk nearly full")
|
Explanation:
__new__ controls instance creation; __init__ runs on every call, so guard with _initialized.
- Double-checked locking (
_lock + second if) avoids creating multiple instances under concurrency.
- Callers use
Logger() or a module-level instance — both resolve to the same object.
- Private state (
_log) centralizes formatting; public methods expose severity levels.
Interview note: In Python, a module-level object (logger = Logger()) is often the idiomatic singleton. Know __new__ for OOP interviews; mention thread safety when asked about multi-threaded servers.
Builder Pattern
Purpose: Construct complex objects step by step, separating the construction process from the final representation.
When to use:
- Many optional fields (avoid telescoping constructors)
- You want immutable products after construction
- The same building steps produce different representations
| from __future__ import annotations
from dataclasses import dataclass
@dataclass(frozen=True)
class Meal:
main_dish: str
side_dish: str
drink: str
dessert: str = "Default Dessert"
appetizer: str = "Default Appetizer"
def summary(self) -> str:
return (
f"Main: {self.main_dish}\n"
f"Side: {self.side_dish}\n"
f"Drink: {self.drink}\n"
f"Dessert: {self.dessert}\n"
f"Appetizer: {self.appetizer}"
)
class MealBuilder:
def __init__(self, main_dish: str, side_dish: str, drink: str) -> None:
self._main_dish = main_dish
self._side_dish = side_dish
self._drink = drink
self._dessert = "Default Dessert"
self._appetizer = "Default Appetizer"
def set_dessert(self, dessert: str) -> MealBuilder:
self._dessert = dessert
return self
def set_appetizer(self, appetizer: str) -> MealBuilder:
self._appetizer = appetizer
return self
def build(self) -> Meal:
return Meal(
main_dish=self._main_dish,
side_dish=self._side_dish,
drink=self._drink,
dessert=self._dessert,
appetizer=self._appetizer,
)
if __name__ == "__main__":
full_meal = (
MealBuilder("Grilled Salmon", "Quinoa", "Sparkling Water")
.set_dessert("Cheesecake")
.set_appetizer("Soup")
.build()
)
simple_meal = MealBuilder("Pasta", "Salad", "Juice").build()
print("Full Meal:\n", full_meal.summary(), sep="")
print("\nSimple Meal:\n", simple_meal.summary(), sep="")
|
Explanation:
Meal is the product — frozen dataclass keeps it immutable after build().
MealBuilder holds required fields in __init__ and optional fields with defaults.
- Fluent setters (
set_dessert, set_appetizer) return self for chaining.
build() validates nothing extra here but is the single place to enforce invariants before returning Meal.
Interview note: Contrast with @dataclass defaults or **kwargs. Builder shines when construction has multiple stages or validation (e.g., "dessert requires main dish").
Factory Method Pattern
Purpose: Define an interface for creating an object, but let subclasses decide which concrete class to instantiate.
When to use:
- A class cannot anticipate the exact type of objects it must create
- Subclasses should control which product gets built
- You want to decouple client code from concrete product classes
| from __future__ import annotations
from abc import ABC, abstractmethod
class Document(ABC):
@abstractmethod
def display_type(self) -> None:
pass
class PDFDocument(Document):
def display_type(self) -> None:
print("Creating a PDF Document")
class WordDocument(Document):
def display_type(self) -> None:
print("Creating a Word Document")
class HTMLDocument(Document):
def display_type(self) -> None:
print("Creating an HTML Document")
class DocumentCreator(ABC):
@abstractmethod
def create_document(self) -> Document:
pass
def export(self) -> None:
document = self.create_document()
document.display_type()
class PDFCreator(DocumentCreator):
def create_document(self) -> Document:
return PDFDocument()
class WordCreator(DocumentCreator):
def create_document(self) -> Document:
return WordDocument()
class HTMLCreator(DocumentCreator):
def create_document(self) -> Document:
return HTMLDocument()
def get_creator(document_type: str) -> DocumentCreator:
creators: dict[str, type[DocumentCreator]] = {
"pdf": PDFCreator,
"word": WordCreator,
"html": HTMLCreator,
}
key = document_type.lower()
if key not in creators:
raise ValueError(f"Unknown document type: {document_type}")
return creators[key]()
if __name__ == "__main__":
for doc_type in ("pdf", "word", "html"):
get_creator(doc_type).export()
|
Explanation:
Document is the product interface; concrete documents implement display_type.
DocumentCreator declares create_document() — the factory method — and uses it in export().
- Each subclass (
PDFCreator, etc.) overrides create_document() to return its product.
get_creator selects the right creator at runtime; clients call export() without knowing concrete classes.
Interview note: Do not confuse with Simple Factory (one function with if/elif) or Abstract Factory (families of related products). Factory Method = one product, subclass decides the type.
Abstract Factory Pattern
Purpose: Provide an interface for creating families of related objects without specifying their concrete classes.
When to use:
- Products must be compatible within a family (e.g., macOS button + macOS checkbox)
- You need to switch entire product families at runtime
- You want to hide concrete class names from client code
| from __future__ import annotations
from abc import ABC, abstractmethod
from dataclasses import dataclass
class Button(ABC):
@abstractmethod
def render(self) -> str:
pass
class Checkbox(ABC):
@abstractmethod
def render(self) -> str:
pass
@dataclass
class WindowsButton(Button):
def render(self) -> str:
return "[Windows Button]"
@dataclass
class WindowsCheckbox(Checkbox):
def render(self) -> str:
return "[Windows Checkbox]"
@dataclass
class MacButton(Button):
def render(self) -> str:
return "(Mac Button)"
@dataclass
class MacCheckbox(Checkbox):
def render(self) -> str:
return "(Mac Checkbox)"
class GUIFactory(ABC):
@abstractmethod
def create_button(self) -> Button:
pass
@abstractmethod
def create_checkbox(self) -> Checkbox:
pass
class WindowsFactory(GUIFactory):
def create_button(self) -> Button:
return WindowsButton()
def create_checkbox(self) -> Checkbox:
return WindowsCheckbox()
class MacFactory(GUIFactory):
def create_button(self) -> Button:
return MacButton()
def create_checkbox(self) -> Checkbox:
return MacCheckbox()
class Application:
def __init__(self, factory: GUIFactory) -> None:
self.button = factory.create_button()
self.checkbox = factory.create_checkbox()
def render(self) -> None:
print(self.button.render())
print(self.checkbox.render())
if __name__ == "__main__":
for factory in (WindowsFactory(), MacFactory()):
app = Application(factory)
app.render()
print()
|
Explanation:
Button and Checkbox are separate product interfaces — the family of related objects.
GUIFactory declares factory methods for every product in the family.
WindowsFactory and MacFactory each return matching concrete products (no mixing Windows button with Mac checkbox).
Application depends only on GUIFactory; swapping the factory swaps the entire UI theme.
Interview note: Abstract Factory is "Factory Method × N products." Common follow-up: adding a new product (e.g., Textbox) forces changes to every concrete factory — trade-off of the pattern.
Prototype Pattern
Purpose: Create new objects by cloning an existing instance instead of building from scratch.
When to use:
- Object construction is expensive or complex
- You need many objects that differ only slightly from a template
- You want to avoid subclassing just to vary configuration
| from __future__ import annotations
import copy
from abc import ABC, abstractmethod
from dataclasses import dataclass, field
@dataclass
class Character(ABC):
name: str
health: int
attack_power: int
defense: int
@abstractmethod
def display(self) -> None:
pass
def clone(self) -> Character:
return copy.deepcopy(self)
@dataclass
class Warrior(Character):
role: str = field(default="Warrior", init=False)
def display(self) -> None:
print(
f"{self.role} - Name: {self.name}, Health: {self.health}, "
f"Attack: {self.attack_power}, Defense: {self.defense}"
)
@dataclass
class Mage(Character):
role: str = field(default="Mage", init=False)
def display(self) -> None:
print(
f"{self.role} - Name: {self.name}, Health: {self.health}, "
f"Attack: {self.attack_power}, Defense: {self.defense}"
)
class CharacterRegistry:
def __init__(self) -> None:
self._prototypes: dict[str, Character] = {}
def register(self, key: str, prototype: Character) -> None:
self._prototypes[key] = prototype
def spawn(self, key: str) -> Character:
if key not in self._prototypes:
raise KeyError(f"Unknown prototype: {key}")
return self._prototypes[key].clone()
if __name__ == "__main__":
registry = CharacterRegistry()
registry.register("warrior", Warrior("Aragorn", 100, 25, 15))
original = registry.spawn("warrior")
cloned = registry.spawn("warrior")
cloned.health = 80
cloned.attack_power = 30
cloned.defense = 10
print("Original:")
original.display()
print("\nCloned (modified):")
cloned.display()
print(f"\nSame object? {original is cloned}") # False
|
Explanation:
Character defines the interface; clone() uses copy.deepcopy for a independent copy.
- Concrete classes (
Warrior, Mage) hold state and implement display().
CharacterRegistry stores prototype templates keyed by name — the prototype manager.
spawn() clones a template; callers tweak the copy without affecting the original or the registry entry.
Interview note: Python's copy.copy vs copy.deepcopy matters when prototypes hold mutable fields (lists, dicts). Mention registry pattern for game entities, config presets, or document templates.