PYTH-270 · Lesson 4 of 12 · 25 min
Creational patterns
▶ Video coming soon A video version of this lesson is on the way.
A design pattern is a named, reusable solution to a problem that keeps coming up in software design. The idea came from the architect Christopher Alexander, who catalogued patterns for buildings and towns, and reached programming through the 1994 book Design Patterns by Erich Gamma, Richard Helm, Ralph Johnson and John Vlissides, known as the Gang of Four. Their catalogue of 23 patterns is grouped into three families: creational patterns, about how objects are created; structural patterns, about how objects are combined; and behavioural patterns, about how objects share work and communicate.
Patterns are valuable mostly as vocabulary. When Lena says the payment code needs a factory, Jordan and Sam know immediately what she means, what the code will roughly look like and what trade-offs come with it. But many patterns were invented to work around limits of languages like C++ and Java, and Python's first-class functions, classes as objects and modules make several of them much simpler or unnecessary. This lesson covers the creational patterns, showing the classic idea and then the Pythonic version, in the context of a real need: Ridgeline is adding a second payment provider.
Why object creation needs design
Creating an object seems trivial: call the class. The trouble starts when the code that needs an object should not have to know which class to create or how to configure it. Ridgeline's checkout code contained the line provider = LumenPay(api_key=..., sandbox=...) in four places. Adding a second provider, Northwind Payments, for wholesale accounts meant finding all four, adding an if around each, and repeating the configuration rules. Creational patterns gather the knowledge of which class to create, and how, into one place.
The underlying principle is the dependency inversion of lesson 2: high-level code such as checkout should depend on the abstract idea of a payment provider, not on the concrete class. Something still has to choose the concrete class, and creational patterns are about where that choice lives.
Factory functions
The simplest creational pattern in Python is a factory function: an ordinary function that decides which class to instantiate and returns the object. Callers ask for a payment provider by name, and only the factory knows the classes and their configuration. Combined with the registry from lesson 2, the factory becomes open for extension: a new provider registers itself, and the factory never changes.
class LumenPay:
def __init__(self, api_key, sandbox=False):
self.api_key, self.sandbox = api_key, sandbox
def charge(self, cents):
return f"LumenPay charged {cents} cents" + (" (sandbox)" if self.sandbox else "")
class NorthwindPayments:
def __init__(self, merchant_id):
self.merchant_id = merchant_id
def charge(self, cents):
return f"Northwind invoice for {cents} cents to merchant {self.merchant_id}"
PROVIDERS = {"lumen": LumenPay, "northwind": NorthwindPayments}
def make_provider(name, **config):
try:
cls = PROVIDERS[name]
except KeyError:
raise ValueError(f"unknown payment provider: {name}") from None
return cls(**config)
online = make_provider("lumen", api_key="test-key", sandbox=True)
wholesale = make_provider("northwind", merchant_id="RCR-0042")
print(online.charge(4595))
print(wholesale.charge(20400))LumenPay charged 4595 cents (sandbox) Northwind invoice for 20400 cents to merchant RCR-0042
Because classes are objects in Python, the registry can hold the classes themselves, and calling cls(**config) creates the instance. In Java the same pattern often needs a factory interface and a factory class per product; in Python a dictionary and a function do the job.
Try it yourself
A notifier factory
Ridgeline sends order updates by email or text message. The classes EmailNotifier(address) and SmsNotifier(phone) are provided. Write make_notifier(kind, **config) that looks the class up in a dictionary called NOTIFIERS (keys "email" and "sms") and returns an instance created with **config.
For an unknown kind, raise ValueError with a message that includes the kind.
class EmailNotifier:
def __init__(self, address):
self.address = address
def send(self, text):
return f"email to {self.address}: {text}"
class SmsNotifier:
def __init__(self, phone):
self.phone = phone
def send(self, text):
return f"sms to {self.phone}: {text}"
def make_notifier(kind, **config):
if kind == "email":
return EmailNotifier(**config)
return SmsNotifier(**config)
print(make_notifier("email", address="theo@example.com").send("Order 1041 packed"))Alternative constructors and the factory method
Sometimes the question is not which class to create but how to create one class from different kinds of input. Python's idiom is the alternative constructor: a class method, conventionally named from_something, that builds an instance from another format. The standard library is full of them: dict.fromkeys, datetime.fromisoformat, int.from_bytes. Ridgeline's Coffee class gains Coffee.from_csv_row and Coffee.from_dict, so the parsing rules live with the class instead of being repeated in every loader.
The Gang of Four's factory method pattern is related: a base class defines an operation that needs a new object, but leaves the choice of class to a method that subclasses override. A report generator might call self.make_formatter() and let PdfReport and CsvReport subclasses return different formatters. In Python, passing the formatter class or a function as a parameter usually achieves the same thing with less machinery.
from dataclasses import dataclass
from decimal import Decimal
@dataclass
class Coffee:
sku: str
name: str
price: Decimal
stock: int
@classmethod
def from_csv_row(cls, row):
sku, name, price, stock = (part.strip() for part in row.split(","))
return cls(sku, name, Decimal(price), int(stock))
@classmethod
def from_dict(cls, data):
return cls(data["sku"], data["name"], Decimal(str(data["price"])), int(data.get("stock", 0)))
print(Coffee.from_csv_row("RC-104, Sumatra Dark, 20.25, 9"))
print(Coffee.from_dict({"sku": "RC-106", "name": "Espresso Ridge", "price": 21.5}))Coffee(sku='RC-104', name='Sumatra Dark', price=Decimal('20.25'), stock=9)
Coffee(sku='RC-106', name='Espresso Ridge', price=Decimal('21.5'), stock=0)Using cls rather than the class name inside the class method matters: if someone subclasses Coffee as SeasonalCoffee, SeasonalCoffee.from_csv_row returns a SeasonalCoffee.
Abstract factory
An abstract factory creates families of related objects that must be used together. Ridgeline's two shop front ends illustrate it: the web shop and the in-cafe kiosk each need a matching receipt formatter, label printer and payment terminal, and mixing a kiosk printer with web receipts produces nonsense. An abstract factory offers methods such as make_receipt(), make_label() and make_payment(), and each concrete factory returns a consistent set.
In Python, an abstract factory is often just an object, or even a module, whose attributes are the matching classes or functions. Passing one factory object around guarantees the parts match, and switching from web to kiosk is a single change where the factory is chosen. Use it when consistency across a family of objects really matters; for a single object, a factory function is enough.
Builder
A builder assembles a complex object step by step, so that the construction reads clearly and validation happens once at the end. Wholesale orders at Ridgeline have many optional parts: a customer, several lines, a delivery date, a purchase order number, a note for the driver and payment terms. A constructor with nine parameters is hard to call correctly; a builder lets the code say what it means, one step at a time, and refuses to build an order that is missing something essential.
class WholesaleOrderBuilder:
def __init__(self, customer):
self._order = {"customer": customer, "lines": [], "terms": "net 30", "po": None, "note": ""}
def add_line(self, sku, bags):
if bags <= 0:
raise ValueError("bags must be positive")
self._order["lines"].append((sku, bags))
return self # returning self allows chaining
def purchase_order(self, number):
self._order["po"] = number
return self
def note(self, text):
self._order["note"] = text
return self
def build(self):
if not self._order["lines"]:
raise ValueError("an order needs at least one line")
return dict(self._order, lines=list(self._order["lines"]))
order = (WholesaleOrderBuilder("Alder Hotel")
.add_line("RC-101", 12)
.add_line("RC-106", 8)
.purchase_order("AH-7731")
.note("Deliver to the loading dock")
.build())
print(order){'customer': 'Alder Hotel', 'lines': [('RC-101', 12), ('RC-106', 8)], 'terms': 'net 30', 'po': 'AH-7731', 'note': 'Deliver to the loading dock'}Python's keyword arguments with defaults remove the need for builders in many cases: Order(customer='Alder Hotel', lines=[...], po='AH-7731') is already readable. Builders earn their place when the object is assembled over several steps, perhaps in different parts of the program, or when validation depends on the whole set of parts.
Try it yourself
A subscription builder
Write SubscriptionBuilder(customer) with chainable methods coffee(sku), every(weeks) and bags(n), each returning self, and a build() method returning a dictionary with keys customer, sku, weeks and bags.
Defaults: weeks is 2 and bags is 1. build() must raise ValueError if no coffee was chosen, and every() must raise ValueError unless weeks is 1, 2 or 4.
class SubscriptionBuilder:
def __init__(self, customer):
self.customer = customer
def coffee(self, sku):
self.sku = sku
def build(self):
return {"customer": self.customer, "sku": self.sku}Caching factories and object pools
Some objects are expensive to create: a connection to the payment provider's servers, a parsed tax table, a compiled template. A caching factory returns the same object for the same arguments instead of creating a new one each time, and functools.cache turns any factory function into one with a single decorator line. An object pool goes a step further for objects that are used briefly and then returned, such as database connections: the pool hands out an idle object, takes it back when the caller is finished, and creates a new one only when all are busy.
Ridgeline uses a caching factory for tax tables: tax_table('BC') parses the province's rules once, and every later call gets the cached table. Database connection pools come from libraries rather than hand-written code; the design lesson is to know they exist and to route every connection request through one factory, so the pooling decision can be changed in one place. Be careful to cache only objects that are safe to share: a cached object that callers modify becomes shared mutable state, the same trap as a singleton.
Singleton, and why Python rarely needs it
The singleton pattern ensures a class has exactly one instance and gives global access to it. It is the most criticised of the original patterns, because a singleton is a global variable in disguise: any code anywhere can reach it, so it hides dependencies, couples distant parts of the program and makes tests interfere with each other through shared state.
Python already has a natural single instance: the module. A module is imported once and cached in sys.modules, so a configured object created at module level is shared by everyone who imports it. When one shared object really is needed, such as the application's settings, create it once at start-up and pass it to the code that needs it, as lesson 2's dependency injection recommends. Reach for module-level state only for things that are genuinely global and safe to share, such as a read-only lookup table.
Prototype: copying a configured object
The prototype pattern creates new objects by copying an existing, configured one rather than building from scratch. Ridgeline's subscription plans start from templates: the Explorer plan, the Espresso plan and the Decaf plan each set the coffee rotation, grind and frequency, and a customer's plan is a copy with a few changes. In Python, copy.copy and copy.deepcopy copy arbitrary objects, and dataclasses.replace makes a copy of a dataclass with chosen fields changed, which suits frozen value objects perfectly.
from dataclasses import dataclass, replace
@dataclass(frozen=True)
class Plan:
name: str
coffees: tuple
grind: str = "whole bean"
weeks: int = 2
EXPLORER = Plan("Explorer", ("RC-102", "RC-103", "RC-104"))
ESPRESSO = Plan("Espresso", ("RC-106",), grind="espresso", weeks=1)
mei = replace(EXPLORER, grind="filter")
alder = replace(ESPRESSO, weeks=2)
print(mei)
print(alder)
print(EXPLORER.grind, "(the template is unchanged)")Plan(name='Explorer', coffees=('RC-102', 'RC-103', 'RC-104'), grind='filter', weeks=2)
Plan(name='Espresso', coffees=('RC-106',), grind='espresso', weeks=2)
whole bean (the template is unchanged)Worked problem
Choosing a creational pattern
Sam has three creation problems in the subscription feature and asks Lena which pattern fits each.
Show solution
- Plans are read from a JSON file and from a database row: two input formats for one class, so alternative constructors, Plan.from_json and Plan.from_row.
- Customers pay with LumenPay or Northwind depending on their account type: the caller should not know the class, so a factory function backed by a registry.
- A customer's plan starts from a standard template with small changes: copy and adjust, so a prototype, using dataclasses.replace.
- Nothing requires assembling a complex object across several steps, so no builder yet: keyword arguments are enough. (YAGNI.)
Answer: Alternative constructors for parsing, a registry-backed factory for providers, and prototypes for plans; no builder or singleton.
Common mistakes
MistakeScattering if-statements that choose concrete classes across the code.
InsteadPut the choice in one factory function, ideally backed by a registry.
MistakeWriting a factory class and interface where a dictionary and a function would do.
InsteadUse Python's first-class classes: a registry maps names to classes directly.
MistakeUsing the class name instead of cls inside a class method.
InsteadUse cls so subclasses get instances of themselves.
MistakeBuilding a builder for an object that keyword arguments already describe clearly.
InsteadAdd a builder only for multi-step assembly or whole-object validation.
MistakeUsing singletons for convenient global access.
InsteadCreate shared objects once and pass them where needed; use modules for genuinely global, read-only data.
MistakeCopying nested objects with copy.copy and then changing the inner parts.
InsteadUse copy.deepcopy, or immutable values with dataclasses.replace.
Summary and key terms
- Design patterns are named solutions to recurring problems; the Gang of Four grouped 23 of them into creational, structural and behavioural families.
- Creational patterns put the decision of which class to create, and how, in one place.
- In Python, a factory function plus a registry of classes covers most factory needs.
- Alternative constructors written as class methods (from_csv_row, from_dict) keep parsing with the class.
- An abstract factory supplies a matching family of objects.
- A builder assembles a complex object step by step and validates it once; keyword arguments often make it unnecessary.
- Singletons are hidden globals; prefer passing shared objects, or module-level state for read-only data.
- Prototypes copy a configured object; dataclasses.replace is the Pythonic way for dataclasses.
- design pattern
- A named, reusable solution to a commonly occurring design problem.
- Gang of Four
- The four authors of the 1994 book Design Patterns.
- creational pattern
- A pattern about how objects are created.
- factory function
- A function that chooses and creates the right kind of object.
- alternative constructor
- A class method that creates an instance from another format.
- factory method
- A method subclasses override to choose which class of object to create.
- abstract factory
- An object that creates a consistent family of related objects.
- builder
- An object that assembles a complex object step by step.
- singleton
- A class restricted to a single, globally accessible instance.
- prototype
- A configured object that new objects are copied from.
Practice problems
Work each problem on paper first, then reveal the answer to check.
What are the three families of Gang of Four patterns?
Show answer
Creational, structural and behavioural.
Why can a Python registry hold classes directly?
Show answer
Classes are objects, so they can be stored in a dictionary and called to create instances.
Name two alternative constructors in the standard library.
Show answer
For example datetime.fromisoformat and dict.fromkeys.
When would you choose a builder over keyword arguments?
Show answer
When the object is assembled over several steps or must be validated as a whole before use.
Why are singletons often considered harmful?
Show answer
They are hidden global state, which couples code and makes tests interfere with each other.
What does dataclasses.replace return?
Show answer
A new instance with the given fields changed and the others copied.
Lesson quiz · 5 questions
Score 60% or more to complete this lesson.
Create a free account to save your progress, take quizzes and earn a certificate.