These cards follow the template from How to Read a Design Pattern: intent, motivation, structure, applicability, consequences, known uses and related patterns. Use them for quick revision before an interview; each card links to the full lesson with complete code.
Singleton#
Classification: Creational (GoF)
Intent: Ensure a class has only one instance and provide a global point of access to it.
Problem: Some things should exist once per application: a connection pool, configuration, a print spooler. A global variable gives access but doesn't stop Database() from being called twice, which wastes resources and splits state.
Structure: a class attribute holding the instance, and a __new__ that creates it once and returns it on every later call.
import threading
class Singleton:
_instance: "Singleton | None" = None
_lock = threading.Lock()
def __new__(cls) -> "Singleton":
if cls._instance is None: # fast path, no lock
with cls._lock:
if cls._instance is None: # re-check under the lock
cls._instance = super().__new__(cls)
return cls._instance
assert Singleton() is Singleton()
Use when: exactly one instance must exist and be reachable from a well-known access point.
| Pros | Cons |
|---|---|
| Controlled access to the sole instance | Hard to unit test; hidden dependencies |
| No polluting global variables | Can be bypassed (e.g. object.__new__(cls)) unless defended |
| Can later be changed to allow a fixed number of instances | Mixes lifecycle management with business logic |
| Needs care to be thread-safe |
Known uses: None, True and False are singletons; logging.getLogger(name) returns one logger per name; imported modules are cached once in sys.modules.
Related: Abstract Factory and Builder instances are often singletons. Prototype is its opposite (many copies).
Simple Factory#
Classification: Creational (an idiom, not a GoF pattern). Also called a static factory method.
Intent: Put object-creation logic in one place, hiding concrete classes from clients.
Problem: if kind == "pizza": return Pizza() followed by elif ... scattered through the codebase couples clients to every concrete class and duplicates creation logic.
Structure: one class with a (usually static) create(kind) method that switches on the type and returns the product interface.
class Product: ...
class ProductA(Product): ...
class ProductB(Product): ...
class SimpleFactory:
@staticmethod
def create(kind: str) -> Product:
match kind:
case "A":
return ProductA()
case "B":
return ProductB()
case _:
raise ValueError(f"Unknown type: {kind}")
Use when: there are a few stable types and you want creation centralized.
| Pros | Cons |
|---|---|
| Centralized creation; clients see only the interface | Adding a type means editing the factory (violates Open/Closed) |
| Very easy to understand | The switch grows with every type |
Known uses: open() returns a text or binary file object depending on the mode; pathlib.Path() returns a PosixPath or WindowsPath.
Related: Factory Method (more flexible, uses inheritance), Abstract Factory (families).
Factory Method#
Classification: Creational (GoF). Also known as virtual constructor.
Intent: Define an interface for creating an object, but let subclasses decide which class to instantiate.
Problem: A framework class needs to create objects but can't know their exact type in advance. Hard-coding ConcreteProduct() in the creator couples it to one product and leaves subclasses no way to customize.
Structure: an abstract creator whose business_logic() calls its own abstract create_product(); each concrete creator overrides that one method.
The client picks a creator and calls the shared logic: CreatorA().business_logic().
Relationships: concrete creators extend the creator, concrete products implement the product interface, each concrete creator creates its product, and business_logic() delegates creation to the subclass.
Use when: a class can't anticipate the class of objects it must create, or wants subclasses to specify them (logistics by road/sea/air, documents in different formats, platform-specific UI).
| Pros | Cons |
|---|---|
| Removes binding to concrete classes | Can require many subclasses |
| Follows Open/Closed and Single Responsibility | More complex than a simple factory |
| Parallel creator and product hierarchies |
Known uses: __iter__(): each collection class returns its own iterator type.
Related: Abstract Factory is often built from factory methods; Template Method often calls factory methods; Prototype is an alternative.
Builder#
Classification: Creational (GoF)
Intent: Separate the construction of a complex object from its representation, so the same construction process can create different representations.
Problem: House(foundation, walls, roof, windows, doors, garage, pool, garden): huge constructors, many optional parameters, unreadable True, False, True arguments, and no easy way to build immutable objects.
Structure: a director (optional) drives a builder interface (build_part_a(), build_part_b(), get_result()); concrete builders assemble the product. The common modern form is a fluent builder whose setters return self:
from dataclasses import dataclass
@dataclass(frozen=True) # the finished product is immutable
class Product:
part_a: str
part_b: str
class ProductBuilder:
def __init__(self) -> None:
self._part_a = ""
self._part_b = ""
def part_a(self, v: str) -> "ProductBuilder":
self._part_a = v
return self
def part_b(self, v: str) -> "ProductBuilder":
self._part_b = v
return self
def build(self) -> Product:
return Product(self._part_a, self._part_b)
product = ProductBuilder().part_a("x").part_b("y").build()
Use when: an object has many optional parameters, you want immutable results, or the same process should produce different representations.
| Pros | Cons |
|---|---|
| Readable construction; no telescoping constructors | More code: a whole builder class |
| Validation before the object exists | Builder duplicates the product's fields |
| Immutable products | Overkill for simple objects |
Known uses: SQLAlchemy's select(...).where(...), Protocol Buffers builders, SQL query builders.
Related: Abstract Factory (also builds complex objects, but in one call); Composite (what builders often build).
Prototype#
Classification: Creational (GoF). Also known as clone.
Intent: Specify the kinds of objects to create using a prototypical instance, and create new objects by copying this prototype.
Problem: Creating an object is expensive (loading textures and animations for every enemy), or you need many near-identical objects. Repeating the setup each time wastes time and duplicates initialization code.
Structure: a Prototype interface with clone(); concrete prototypes implement it; the client asks a prototype to clone itself instead of calling the constructor.
import copy
class Enemy:
def __init__(self) -> None:
self.abilities: list[str] = []
def clone(self) -> "Enemy":
new = copy.copy(self) # shallow copy
# Deep-copy mutable fields
new.abilities = list(self.abilities)
return new
Use when: creation is expensive, you need many similar objects with small variations, or the classes to create are chosen at runtime.
| Pros | Cons |
|---|---|
| Avoids repeated expensive setup | Deep copying is easy to get wrong |
| Hides concrete classes; prototypes can be registered at runtime | Circular references complicate cloning |
| Less subclassing | Every class needs a correct clone() |
Known uses: Python's copy.copy / copy.deepcopy (customizable with __copy__ / __deepcopy__), JavaScript's prototype-based objects.
Related: Factory Method (create from scratch instead); Abstract Factory can store prototypes; Composite trees are often cloned.
Strategy#
Classification: Behavioral (GoF). Also known as policy.
Intent: Define a family of algorithms, encapsulate each one, and make them interchangeable.
Problem: if kind == "A": ... / elif kind == "B": ... for sorting, payments or routing. Big conditionals, hard to add algorithms, violates Open/Closed.
Structure: the context holds a reference to the strategy interface and delegates to it; concrete strategies implement that interface.
from abc import ABC, abstractmethod
class Strategy(ABC):
@abstractmethod
def execute(self) -> None: ...
class Context:
def __init__(self, strategy: Strategy) -> None:
self._strategy = strategy
def set_strategy(self, s: Strategy) -> None:
self._strategy = s
def execute_strategy(self) -> None:
self._strategy.execute()
Use when: related classes differ only in behavior, you need algorithm variants, or a class is full of conditionals selecting behavior (payments, routes, compression, sorting).
| Pros | Cons |
|---|---|
| Interchangeable algorithm families | Clients must know the strategies to choose one |
| Eliminates conditionals; Open/Closed | Context and strategy must agree on the data passed |
| More objects |
Known uses: the key= function passed to sorted() and list.sort(), the default= hook of json.dumps().
Related: State (same structure, different intent); Template Method (varies steps via inheritance instead); stateless strategies can be shared as flyweights.
Observer#
Classification: Behavioral (GoF). Also known as publish-subscribe, dependents.
Intent: Define a one-to-many dependency between objects so that when one object changes state, all its dependents are notified and updated automatically.
Problem: A weather station calls updateCurrentDisplay(), updateStatsDisplay(), updateForecast() directly. It knows every display, adding one means editing the station, and displays can't come and go at runtime.
Structure: the subject keeps a list of observers with attach() and detach() and notifies them on every change; observers implement update().
from typing import Protocol
class Observer(Protocol):
def update(self, temp: float, humidity: float) -> None: ...
class WeatherStation:
def __init__(self) -> None:
self._observers: list[Observer] = []
self.temperature = 0.0
self.humidity = 0.0
def attach(self, o: Observer) -> None:
self._observers.append(o)
def detach(self, o: Observer) -> None:
self._observers.remove(o)
def set_measurements(self, t: float, h: float) -> None:
self.temperature, self.humidity = t, h
for o in self._observers:
o.update(self.temperature, self.humidity)
Use when: a change to one object requires changing others, you don't know how many, and the subject shouldn't know who they are (newsletters, stock alerts, GUI events).
| Pros | Cons |
|---|---|
| Loose coupling between subject and observers | Notification order isn't guaranteed |
| Observers added and removed at runtime | Forgotten observers leak memory |
| Broadcast communication | Update cascades are hard to debug; many observers slow notification |
Known uses: Tkinter event callbacks, logging handlers, JavaScript EventEmitter and DOM events, .NET events, model-to-view updates in MVC.
Related: Mediator (centralizes communication); an event bus is a centralized variant.
Key takeaways#
- Every card answers the same questions: intent, problem, structure, when to use, trade-offs.
- Read the problem line first; it tells you when the pattern applies.
- Check the cons before choosing a pattern; each one has real costs.
- Use the links for full, runnable examples.