Creational Patterns
Patterns for creating objects: factory method, abstract factory, builder and singleton, what problem each solves, and when a plain function is the better choice.
Every object-oriented program creates objects, and most of the time
a plain constructor call such as Invoice(...) is all
you need. Creational patterns are for the cases where that call
becomes a problem: the right class depends on data, the object needs
many parts that must fit together, or many parts of the program need
the same instance. The examples follow KBS, Kestrel Energy's billing
system, which turns contracts and meter readings into invoices. The
object-oriented basics (classes, interfaces, abstract classes) are
on
Object-Oriented Concepts; the principles behind the patterns, such as depending on
abstractions, are on
Design Principles.
What a design pattern is
A design pattern is a named, reusable solution to a problem that keeps coming back in software design, described together with the situation it fits and the consequences of using it. A pattern is not a piece of library code you import; it is an idea you implement in your own code, in your own language. The idea of a "pattern language" comes from the architect Christopher Alexander, who described recurring solutions in buildings and towns in the 1970s.
The pattern names that developers use today mostly come from one book: Design Patterns: Elements of Reusable Object-Oriented Software (1994) by Erich Gamma, Richard Helm, Ralph Johnson and John Vlissides. The four authors are known as the Gang of Four (GoF), and the 23 patterns in their book are called the GoF patterns. They fall into three groups:
| Group | About | Patterns |
|---|---|---|
| Creational (5) | creating objects | Abstract Factory, Builder, Factory Method, Prototype, Singleton |
| Structural (7) | combining objects into larger structures | Adapter, Bridge, Composite, Decorator, Facade, Flyweight, Proxy |
| Behavioral (11) | how objects divide work and talk to each other | Chain of Responsibility, Command, Interpreter, Iterator, Mediator, Memento, Observer, State, Strategy, Template Method, Visitor |
The book's real value is the shared vocabulary. "Put a factory in front of the tariff classes" says in five words what would otherwise take a paragraph, in a code review, an architecture discussion or a ticket. The examples in the book are written in C++ and Smalltalk, though, and some patterns exist mainly to work around limits of those languages. Peter Norvig argued in 1996 that 16 of the 23 patterns are "either invisible or simpler" in dynamic languages with first-class functions and types. Python is such a language, so each section below shows the pattern and then the shorter Python form.
Factory method
KBS has several tariff types. A fixed tariff has one price per
kilowatt-hour (kWh); a day/night tariff has a day and a night price.
The contract record in the database says which type a customer has.
Without a factory, every piece of code that needs a tariff contains
its own if tariff == "fixed" chain, and adding a third
type means finding all of them.
A factory method is a method or function whose job is to create an object, so that the caller does not decide, or even know, which concrete class it gets back. In the GoF form, a base class declares the creation method and each subclass overrides it to create a different product. In everyday use the name covers any function or method that hides the choice of class, and in Python that is usually a dictionary plus a function:
from dataclasses import dataclass
from decimal import Decimal
@dataclass(frozen=True)
class FixedTariff:
price: Decimal # euros per kWh, day and night
@classmethod
def from_row(cls, row): # alternative constructor
return cls(Decimal(row["price"]))
def cost(self, kwh, hour):
return kwh * self.price
@dataclass(frozen=True)
class DayNightTariff:
day: Decimal
night: Decimal
@classmethod
def from_row(cls, row):
return cls(Decimal(row["day"]), Decimal(row["night"]))
def cost(self, kwh, hour):
price = self.night if hour < 7 or hour >= 23 else self.day
return kwh * price
TARIFF_TYPES = {"fixed": FixedTariff, "day_night": DayNightTariff}
def tariff_from_contract(row):
"""Factory: the contract record decides which class is created."""
try:
tariff_class = TARIFF_TYPES[row["tariff"]]
except KeyError:
raise ValueError(f"unknown tariff type {row['tariff']!r}") from None
return tariff_class.from_row(row)
contracts = [
{"contract": "CT-20417", "tariff": "fixed", "price": "0.2700"},
{"contract": "CT-20418", "tariff": "day_night", "day": "0.3120", "night": "0.2240"},
{"contract": "CT-20419", "tariff": "Fixed", "price": "0.2790"},
]
for row in contracts:
try:
tariff = tariff_from_contract(row)
except ValueError as error:
print(f"{row['contract']}: {error}")
continue
# The billing code only calls cost(); it never names a tariff class.
print(f"{row['contract']}: {type(tariff).__name__:14} "
f"4 kWh at 02:00 = {tariff.cost(4, 2):.2f}, at 14:00 = {tariff.cost(4, 14):.2f}")
CT-20417: FixedTariff 4 kWh at 02:00 = 1.08, at 14:00 = 1.08 CT-20418: DayNightTariff 4 kWh at 02:00 = 0.90, at 14:00 = 1.25 CT-20419: unknown tariff type 'Fixed'
Three things make this a good factory:
-
The decision lives in one place. A new tariff
type is a new class plus one line in
TARIFF_TYPES; the billing loop does not change. This is the open-closed idea from Design Principles. -
Callers depend on behavior, not on classes. The
loop only calls
cost(), the shared interface of both classes. -
Bad data fails clearly. The unknown type
"Fixed"(a capital F typed by hand into the contract record) gives a readableValueErrorat the moment the contract is read, not a confusingAttributeErrorthree functions later.
The from_row methods are the idiomatic Python version
of a factory method on the class itself: a
@classmethod that works as an alternative constructor.
The standard library is full of them, such as
datetime.fromisoformat() and
dict.fromkeys(). And because Python classes are
ordinary objects that can be stored in a dictionary and called, the
registry TARIFF_TYPES replaces the parallel class
hierarchy of "creator" classes that the GoF version needs.
In a statically typed language, the factory's return type makes the
same point explicitly. In TypeScript the factory can return plain
objects that match an
interface; thanks to
structural typing
they need no class at all, and the compiler checks that the
switch covers every contract type:
interface Tariff {
readonly kind: string;
cost(kwh: number, hour: number): number;
}
type ContractRow =
| { contract: string; tariff: "fixed"; price: number }
| { contract: string; tariff: "day_night"; day: number; night: number };
// The factory returns the interface; callers never see a class name.
function createTariff(row: ContractRow): Tariff {
switch (row.tariff) {
case "fixed": {
const price = row.price;
return { kind: "fixed", cost: (kwh) => kwh * price };
}
case "day_night": {
const { day, night } = row;
return {
kind: "day_night",
cost: (kwh, hour) => kwh * (hour < 7 || hour >= 23 ? night : day),
};
}
}
}
const rows: ContractRow[] = [
{ contract: "CT-20417", tariff: "fixed", price: 0.27 },
{ contract: "CT-20418", tariff: "day_night", day: 0.312, night: 0.224 },
];
for (const row of rows) {
const tariff = createTariff(row);
console.log(`${row.contract}: ${tariff.kind}, 4 kWh at 14:00 = ${tariff.cost(4, 14).toFixed(2)}`);
}
CT-20417: fixed, 4 kWh at 14:00 = 1.08 CT-20418: day_night, 4 kWh at 14:00 = 1.25
If someone adds a third member to ContractRow and
forgets the factory, tsc --strict reports that the
function can end without returning a Tariff: the
missing case is found at compile time instead of in the nightly
billing run.
Abstract factory
Sometimes the choice is not one class but a whole set of classes
that must match. Joost Bakker's nightly export at 02:30 writes the
same invoices for two receivers: the bank wants a
semicolon-separated CSV file with one naming scheme, the accounting
system wants XML with another. Mixing them up, an XML body in a
.csv
file, is exactly the kind of bug that only shows up at the bank.
An abstract factory is an object that creates a family of related objects through one interface, so the code that uses them can switch the whole family at once without naming any concrete class, and cannot combine parts of different families by accident:
from abc import ABC, abstractmethod
class ExportFactory(ABC):
"""Abstract factory: creates the matching parts of ONE export format."""
@abstractmethod
def create_writer(self): ...
@abstractmethod
def create_namer(self): ...
class CsvWriter:
def render(self, rows):
lines = ["invoice;account;amount"]
lines += [f"{r['invoice']};{r['account']};{r['amount']}" for r in rows]
return "\n".join(lines)
class XmlWriter:
def render(self, rows):
lines = ["<invoices>"]
lines += [f' <invoice id="{r["invoice"]}" account="{r["account"]}" '
f'amount="{r["amount"]}"/>' for r in rows]
lines.append("</invoices>")
return "\n".join(lines)
class BankExportFactory(ExportFactory): # family 1: the bank
def create_writer(self):
return CsvWriter()
def create_namer(self):
return lambda day: f"kestrel-collect-{day}.csv"
class LedgerExportFactory(ExportFactory): # family 2: the accounting system
def create_writer(self):
return XmlWriter()
def create_namer(self):
return lambda day: f"KESTREL_{day.replace('-', '')}.xml"
def nightly_export(factory, rows, day):
# Never names a format: whatever the factory makes belongs together.
writer = factory.create_writer()
file_name = factory.create_namer()(day)
print(f"--- {file_name}")
print(writer.render(rows))
rows = [
{"invoice": "INV-2026-104412", "account": "C-10442", "amount": "118.65"},
{"invoice": "INV-2026-104413", "account": "C-10587", "amount": "96.10"},
]
for factory in (BankExportFactory(), LedgerExportFactory()):
nightly_export(factory, rows, "2026-10-07")
--- kestrel-collect-2026-10-07.csv invoice;account;amount INV-2026-104412;C-10442;118.65 INV-2026-104413;C-10587;96.10 --- KESTREL_20261007.xml <invoices> <invoice id="INV-2026-104412" account="C-10442" amount="118.65"/> <invoice id="INV-2026-104413" account="C-10587" amount="96.10"/> </invoices>
nightly_export asks the factory for a writer and a
namer, and whatever it gets belongs together. Adding a third
receiver means one new factory class; the export function stays the
same. GUI toolkits are the textbook case: a "Windows" factory and a
"macOS" factory that each create buttons, menus and dialogs in one
consistent style.
Builder
An invoice has a number, a customer, any number of lines, an
optional discount, a VAT (value-added tax) percentage and a
language. A constructor with seven positional parameters is hard to
call correctly:
Invoice("INV-1", "C-1", lines, 0, 21, "en", True) does
not say what True means. The
builder pattern moves the
construction into a separate object that collects the parts step by
step, with one named method per part, and creates the finished,
usually immutable, object in a final build() call that
also checks that everything fits.
Java shows the pattern well, because Java has no keyword arguments.
The invoice itself is a
record
(immutable), and a nested static Builder class collects
the parts:
import java.math.BigDecimal;
import java.math.RoundingMode;
import java.util.ArrayList;
import java.util.List;
public class Main {
public static void main(String[] args) {
Invoice invoice = Invoice.builder("INV-2026-104412")
.customer("C-10442")
.line("Electricity, October", "86.40")
.line("Network costs", "31.25")
.discount("5.00")
.build();
System.out.println(invoice.number() + " for " + invoice.customer()
+ ": " + invoice.lines().size() + " lines, total " + invoice.total());
try {
Invoice.builder("INV-2026-104413").line("Electricity, October", "64.10").build();
} catch (IllegalStateException e) {
System.out.println("Rejected: " + e.getMessage());
}
}
}
record Line(String description, BigDecimal amount) {}
// The finished object is immutable: a record with a copied, unmodifiable list.
record Invoice(String number, String customer, List<Line> lines,
BigDecimal discount, int vatPercent) {
Invoice {
lines = List.copyOf(lines);
}
static Builder builder(String number) {
return new Builder(number);
}
BigDecimal total() {
BigDecimal net = lines.stream().map(Line::amount)
.reduce(BigDecimal.ZERO, BigDecimal::add).subtract(discount);
BigDecimal vat = net.multiply(BigDecimal.valueOf(vatPercent)).movePointLeft(2);
return net.add(vat).setScale(2, RoundingMode.HALF_EVEN);
}
// The builder is mutable and collects the parts step by step.
static final class Builder {
private final String number;
private String customer;
private final List<Line> lines = new ArrayList<>();
private BigDecimal discount = BigDecimal.ZERO;
private int vatPercent = 21;
private Builder(String number) {
this.number = number;
}
Builder customer(String customer) {
this.customer = customer;
return this;
}
Builder line(String description, String amount) {
lines.add(new Line(description, new BigDecimal(amount)));
return this;
}
Builder discount(String amount) {
this.discount = new BigDecimal(amount);
return this;
}
Builder vatPercent(int percent) {
this.vatPercent = percent;
return this;
}
Invoice build() { // all checks in one place
if (customer == null) {
throw new IllegalStateException("invoice " + number + " has no customer");
}
if (lines.isEmpty()) {
throw new IllegalStateException("invoice " + number + " has no lines");
}
return new Invoice(number, customer, lines, discount, vatPercent);
}
}
}
INV-2026-104412 for C-10442: 2 lines, total 136.31 Rejected: invoice INV-2026-104413 has no customer
The calling code reads like a description of the invoice, each
method returns the builder so the calls can be chained (a
fluent interface), defaults such as the 21% VAT live in one
place, and an invoice without a customer can never exist: the check
in build() rejects it before the object is created. The
Java standard library uses builders the same way, for example
StringBuilder and
HttpRequest.newBuilder() in java.net.http.
Python: keyword arguments instead
Python already solves most of what a builder solves. Keyword
arguments name every part, defaults make parts optional, a
dataclass
with frozen=True makes the result immutable,
__post_init__ is the place for the checks, and
dataclasses.replace() makes a changed copy:
from dataclasses import dataclass, replace
from decimal import Decimal
@dataclass(frozen=True)
class Invoice:
number: str
customer: str
lines: tuple = ()
discount: Decimal = Decimal("0.00")
vat_percent: int = 21
language: str = "nl"
def __post_init__(self): # validation in one place
if not self.lines:
raise ValueError(f"invoice {self.number} has no lines")
# Keyword arguments with defaults: no builder needed for most objects.
october = Invoice(
number="INV-2026-104412",
customer="C-10442",
lines=(("Electricity, October", Decimal("86.40")),
("Network costs", Decimal("31.25"))),
language="en",
)
print(october.discount, october.vat_percent, october.language)
# A changed copy instead of a mutable object with setters.
corrected = replace(october, discount=Decimal("5.00"))
print(corrected.discount, october.discount)
try:
Invoice(number="INV-2026-104413", customer="C-10587")
except ValueError as error:
print("Rejected:", error)
0.00 21 en 5.00 0.00 Rejected: invoice INV-2026-104413 has no lines
A builder still helps in Python when the object really is assembled over time, for example lines added one by one while reading a file, or when a library offers a fluent interface for building queries or HTTP requests. For an object with three fields it is ceremony.
Singleton
A singleton is a class that allows only one instance of itself and gives the whole program a global way to reach that instance. Typical candidates are things that are expensive to create or should exist once: a configuration object, a cache, a connection pool, a table of current tariffs.
In Python you rarely write the class-based version, which overrides
__new__ so every call returns the same object. A
module
is already a singleton: it is imported once and cached in
sys.modules, so a module-level object is shared by
everyone who imports it. When the object should only be created on
first use, functools.cache on a function does it in one
line:
import functools
class TariffTable:
def __init__(self):
print("Loading tariff table (expensive) ...")
self.prices = {"day_night": "0.3120", "fixed": "0.2700"}
@functools.cache
def tariff_table():
"""Returns the same TariffTable every time: created on first use."""
return TariffTable()
first = tariff_table()
second = tariff_table()
print(first is second)
print(tariff_table.cache_info())
Loading tariff table (expensive) ... True CacheInfo(hits=1, misses=1, maxsize=None, currsize=1)
Why singletons cause trouble
"There is only one tariff table" is true today. The trouble is the global access half of the pattern:
-
Hidden dependencies. A function that calls
tariff_table()inside its body depends on it, but its signature does not say so. You find out by reading every line, or when it fails. - Hard to test. A test cannot hand in a small fake table; it gets the real one, which in KBS's case loads from db01. Tests that share a global object also influence each other.
- "Only one" stops being true. A second region, a test with two configurations or a migration that runs old and new tariffs side by side all need two instances of something that was built to allow one.
The alternative keeps the "create it once" and drops the global access: the program's start-up code creates one instance and passes it to everything that needs it, usually as a constructor argument. That technique is dependency injection, and the place where the objects are created and wired together is called the composition root; both are explained on Layers and Boundaries.
from decimal import Decimal
class TariffTable:
def __init__(self, prices):
self.prices = prices
class InvoiceCalculator:
def __init__(self, tariffs): # the dependency is visible here
self.tariffs = tariffs
def cost(self, tariff, kwh):
return kwh * Decimal(self.tariffs.prices[tariff])
# Production: the start-up code creates ONE table and passes it on.
shared = TariffTable({"day_night": "0.3120", "fixed": "0.2700"})
calculator = InvoiceCalculator(shared)
print(f"{calculator.cost('fixed', 250):.2f}")
# A test hands in its own small table: no database, no global state to reset.
test_calculator = InvoiceCalculator(TariffTable({"fixed": "0.10"}))
print(f"{test_calculator.cost('fixed', 250):.2f}")
67.50 25.00
Prototype
The fifth creational pattern is the least used. The
prototype pattern creates
new objects by copying an existing, ready-configured object, the
prototype, instead of building each one from scratch. Lucía Romero's
contract templates are a natural fit: a new fixed-price contract
starts as a copy of the template and changes only what differs.
Python's copy module does the copying, and the one
thing to understand is the difference between a shallow and a deep
copy:
import copy
template = {
"tariff": "fixed",
"price": "0.2700",
"advance_payment_day": 1,
"notes": [],
}
shallow = copy.copy(template) # new dict, but the SAME notes list
shallow["notes"].append("customer moved in on 2026-10-01")
print(template["notes"]) # the template changed too
template["notes"].clear()
deep = copy.deepcopy(template) # copies the nested list as well
deep["notes"].append("customer moved in on 2026-10-01")
deep["price"] = "0.2790"
print(template["notes"], template["price"])
print(deep["notes"], deep["price"])
['customer moved in on 2026-10-01'] [] 0.2700 ['customer moved in on 2026-10-01'] 0.2790
A shallow copy creates a new outer object but shares everything
inside it, so a note added to one contract appeared in the template.
copy.deepcopy() copies nested objects as well. For
frozen dataclasses, dataclasses.replace() (shown under
Builder) is the cleaner way to get
a changed copy.
When a plain function is the better choice
The patterns on this page were described mainly with C++ in mind, as it was in 1994: classes were not values at run time, there were no closures to pass around, and every argument was positional. Python has none of those limits, which is why the Python versions above keep getting shorter:
| Pattern | Problem it solves | Usually enough in Python |
|---|---|---|
| Factory method | which class to create depends on data |
a dictionary of classes, a function, a
@classmethod constructor
|
| Abstract factory | a family of parts that must match | a dictionary of modules or small objects |
| Builder | many optional parts, checks on the whole |
keyword arguments, a dataclass with
__post_init__, replace()
|
| Singleton | exactly one shared instance |
one instance created at start-up and passed in; a module;
functools.cache
|
| Prototype | new objects from a configured template |
copy.deepcopy(),
dataclasses.replace()
|
That does not make the patterns useless in Python. The names still help a team talk about code, and the problems they describe are real. In Java, C# and TypeScript, which have interfaces and static types, the classic shapes are more common and often clearer, as the Java builder above shows. A good rule in any language: write the simple version first, and introduce the pattern when the second or third variation turns up and the simple version starts to hurt.