*Published on 11/05/2025* # Builder method design pattern I read a python class definition recently with so many `__init__` arguments that a fist fight broke out. Let's just say... it wasn't pretty. "If only they'd known about the builder method, then they'd be able to get along," I thought. ### The builder method The builder pattern is a [[factory method design pattern#Creational design patterns|creational design pattern]] that seeks to make creating objects flexible by separating the object from the construction of the object. In Python, you can see this pattern in action in a variety of popular libraries: - SQLAlchemy queries (`select().where().order_by()`) - Plotly charts (step-by-step figure construction) - PySpark (`SparkSession.builder.appName().getOrCreate()`) You can see in PySpark, for example, that when you load data into a data frame, there are a lot of options. The library does a good job of setting reasonable defaults for all of the options, but by utilizing the builder method, you can set each of those options in a readable and flexible way for the use case at hand. ```python from pyspark.sql import SparkSession spark = SparkSession.builder.appName("FileLoaderExample").getOrCreate() df = ( spark.read .format("csv") .option("header", "true") .option("inferSchema", "true") .option("delimiter", ",") .load("/mnt/data/sales.csv") ) df.show() ``` The builder pattern involves three main parts: 1. **Product** — The complex object you're trying to create 2. **Builder** — The builder is the object responsible for constructing the product 3. **Director** (optional) — An object that helps to define the ordering of construction steps This pattern is particularly useful in the following cases: - You need to construct a complex object with many optional parameters - You need to separate the construction logic from the object representation - You need to create an immutable object with some logic in the construction of that object Let's look at an example. ### An example Imagine that you've got an object that defines a loan application for a banking application you're creating. Those things are very complex! There's a ton of information that we will need to fill into the loan application to make sure everything is accounted for. For the sake of the example, we'll simplify slightly to not include *everything* you might need for a loan application, but hopefully enough to get the point across. In python, if we were to create a loan application class, it might look something like this. ```python class LoanApplication: def __init__( self, applicant_name, amount, term_years, loan_type=None, interest_rate=None, collateral=None, employer_name=None, annual_income=None, existing_debts=None, ): self.applicant_name = applicant_name self.amount = amount self.term_years = term_years self.loan_type = loan_type self.interest_rate = interest_rate self.collateral = collateral self.employer_name = employer_name self.annual_income = annual_income self.existing_debts = existing_debts def __str__(self): return ( f"Loan application for {self.applicant_name}: " f"{self.amount} over {self.term_years} years" ) def ammortization_schedule(self): pass ``` Yep, that's a lot of optional arguments. In this case, we may also want the application to be immutable, and separating the construction of the object from the object itself will certainly simplify things and give us some additional flexibility. So, how do we implement the builder method? Well, we know there are some required arguments for the loan application class, so we can build those into the new builder class with some added validation on those required parameters. ```python class LoanApplicationBuilder: def __init__(self, applicant_name: str, amount: float, term_years: int): if not applicant_name or amount <= 0 or term_years <= 0: raise ValueError("Invalid required parameters") self._application = LoanApplication( applicant_name=applicant_name, amount=amount, term_years=term_years ) ``` Next, we can define each of the optional parameters with a setter function to update the LoanApplication object we are storing in the builder. ```python class LoanApplicationBuilder: # __init__ def set_loan_type(self, loan_type: str) -> Self: self._application.loan_type = loan_type return self def set_interest_rate(self, interest_rate: float) -> Self: if interest_rate < 0: raise ValueError("Interest rate cannot be negative") self._application.interest_rate = interest_rate return self # etc. for the rest of the optional parameters ``` Notice here that we are returning self, which is the instance of the LoanApplicationBuilder. This will allow us to chain these different function calls to modify the loan application. To finish the builder class, we need some way for the builder to return the product, and we'll do that with an additional build method. ```python class LoanApplicationBuilder: # existing code def build(self) -> LoanApplication: return self._application ``` Finally, to make this whole thing more pythonic, we can modify the product (`LoanApplication`) to use a `dataclass` in order to clean up a lot of the boilerplate code we wrote before with the optional arguments. You can see that in [GitHub](https://www.github.com/mrjaketomlinson/jacobwritescode). 😁 Now that we've got the builder method written, how do we use it? First, we instantiate the builder with the required arguments. Then, we can chain any of the optional methods to the builder itself to update the application. Finally, we can call the `build()` method to return the `LoanApplication` object. ```python builder = LoanApplicationBuilder("John Doe", 250000, 30) loan_application = ( builder.set_loan_type("Mortgage") .set_interest_rate(3.5) .set_employer_name("Tech Corp") .set_annual_income(85000) .set_existing_debts(15000) .build() ) ``` What's great is this is extremely readable. It's also incredible flexible, allowing us to create different versions of loans (e.g., mortgage, car loan, etc.), which have different optional parameters we could want to specify, without needing different products. Now that we've implemented the builder method, there aren't any more fights, at least until the next design session starts up. 😉 Happy coding!