The Builder pattern — constructing complex objects step by step
When a constructor grows enough optional parameters that call sites stop being readable, Builder is the pattern that separates "how to build this thing" from "what this thing actually is."
4 min read
The problem: a constructor with too many optional parameters
class Pizza:
def __init__(self, size, cheese=True, pepperoni=False, mushrooms=False,
olives=False, extra_sauce=False, thin_crust=False, gluten_free=False):
self.size = size
self.cheese = cheese
self.pepperoni = pepperoni
# ...six more assignments
Pizza("large", True, False, True, False, True, False, False)That call site is unreadable — every position is a boolean, and there's no way to tell what True, False, True, False, True actually mean without counting positions against the constructor's signature. Adding a ninth option makes it worse, and every existing call site is now one silent miscount away from ordering pepperoni on a pizza that was meant to be plain. Keyword arguments help (Pizza("large", pepperoni=True)), but once an object genuinely has many optional pieces that get assembled in some order, or need validation partway through construction, a plain constructor — keyword arguments or not — stops being the right tool.
The fix: a separate object whose whole job is building
class Pizza:
def __init__(self, size, toppings, crust_type):
self.size = size
self.toppings = toppings
self.crust_type = crust_type
class PizzaBuilder:
def __init__(self, size):
self._size = size
self._toppings = []
self._crust_type = "regular"
def add_pepperoni(self):
self._toppings.append("pepperoni")
return self
def add_mushrooms(self):
self._toppings.append("mushrooms")
return self
def thin_crust(self):
self._crust_type = "thin"
return self
def build(self):
return Pizza(self._size, self._toppings, self._crust_type)
pizza = (
PizzaBuilder("large")
.add_pepperoni()
.add_mushrooms()
.thin_crust()
.build()
)Every step is now a named method call instead of a positional boolean — add_pepperoni() says exactly what it does, in a way True in the fourth position never could. Each builder method returns self, which is what enables method chaining — calling one method after another on the same line, each one configuring one more piece before .build() finally constructs the real Pizza. The Pizza class itself stays simple: it just holds the final, fully-assembled data, with no knowledge of how it was built.
Why this is different from just adding more constructor parameters
# The Pizza class never has an invalid intermediate state during construction —
# it's only created once, at .build(), with everything already decided.
# Compare to a constructor with 8 positional booleans: every call site
# has to get every position right, all at once, with no room to build up
# the object incrementally or validate as you go.The real structural difference is when validation and assembly can happen. A builder can check a partial state as it's being built (add_pepperoni() could reject that call if size was "small" and a topping limit applies), accumulate a variable-length collection (_toppings) that doesn't map cleanly onto a fixed constructor signature at all, and only actually construct the target object once everything is settled and consistent — a constructor gets exactly one shot, with everything provided at once, and can't easily support any of that.
Method chaining and the fluent interface
query = (
QueryBuilder("users")
.select("name", "email")
.where("age > 18")
.order_by("name")
.limit(10)
.build()
)This exact shape — a chain of method calls, each returning self, ending in a terminal call that produces the real object — is called a fluent interface, and it shows up constantly beyond toy pizza examples: Django's own QuerySet chaining (Book.objects.filter(...).exclude(...).order_by(...), from the querysets-are-lazy lesson) and SQL query builders in many ORMs use exactly this pattern. It isn't always a formal "Builder" with a .build() step — QuerySets stay lazy and chainable without one — but the underlying mechanism (each method call returns something that supports the next call) is the same idea.
The concrete signal Builder belongs somewhere
A constructor accumulating enough optional parameters that call sites become genuinely hard to read at a glance — especially several same-typed parameters in a row (multiple booleans, multiple strings) where a mistaken order compiles fine and fails silently — is the tell. A class with two or three required parameters and no meaningful optional configuration doesn't need a builder; the pattern earns its keep once "constructing this correctly" is itself a multi-step process worth naming, not just a longer parameter list.
Further reading
Check your understanding
A quick comprehension check — not tracked, not graded, just for you.
1. What's the core readability problem with Pizza('large', True, False, True, False, True, False, False)?
2. Why does each method on PizzaBuilder (like add_pepperoni()) return self?
3. What can a Builder do that a plain constructor structurally can't?
4. How does Django's QuerySet chaining relate to the Builder pattern's fluent interface, despite having no .build() method?