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 readable ValueError at the moment the contract is read, not a confusing AttributeError three 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.

The billing run passes a contract row to the tariff factory and gets a Tariff back. The factory creates either a FixedTariff or a DayNightTariff; both offer cost(), so the billing run never needs to know which one it got. billing run tariff factory from_row() contract row a Tariff FixedTariff cost() DayNightTariff cost() creates one
A factory. The caller hands over data and gets back something with the interface it needs; which concrete class was created is the factory's business.

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.