Card 06/ 09

ComparisonDifficulty: Intermediate2 min

Builder Classes vs Keyword Arguments in Python

Named parameters with defaults already solve the telescoping constructor in Python.

In Python, keyword arguments with default values give every parameter a name at the call site, which removes the problem the Builder pattern was invented for. This card writes a report with optional sections without a builder, and names the cases where a builder still earns its class.

py
from dataclasses import dataclass
from typing import Optional


@dataclass(frozen=True)
class Report:
    title: str
    summary: Optional[str] = None
    chart: Optional[str] = None
    footer: Optional[str] = None

    def __post_init__(self) -> None:
        if not self.title:
            raise ValueError("a report needs a title")


print(Report("Weekly sales", chart="orders_by_day", footer="Generated on Monday"))
print(Report(title="Stock check"))
try:
    Report("", summary="no title")
except ValueError as error:
    print("refused:", error)
OutputRan with python:3.12-slim (Python 3.12.13)
Report(title='Weekly sales', summary=None, chart='orders_by_day', footer='Generated on Monday')
Report(title='Stock check', summary=None, chart=None, footer=None)
refused: a report needs a title
Defaults make every optional part optional, and `__post_init__` checks the required one.

Each optional part has a default of None, so a call names only the parts it sets, in any order. The dataclass is frozen, so the report cannot change after it is created, and __post_init__, which a dataclass calls after its generated constructor, is the one place that checks it, as build() is in a builder. This gives the report everything a builder would, with one class.

Where each form fits in Python

Keyword argumentswhen every part is known at the call

There is one class and one call. Names and defaults make a long parameter list readable, and validation goes in __init__ or __post_init__. This is the default for a Python object with many optional parts.

A builderwhen the object is assembled over time

Parts arrive across several statements or a loop, a part can be added many times, such as the conditions of a query, or one sequence of steps must produce more than one representation. A builder holds the unfinished state until build().

A builder in Python

Use it when

  • The object is put together over several statements, with parts added conditionally.
  • A part accumulates, such as filters or rows, rather than being set once.
  • The same steps must drive more than one representation, which is the Gang of Four's case.

Avoid it when

  • Every part is known where the object is created, which keyword arguments already handle.
  • The only goal is readable names at the call site.