Python
Completed
-
Section 1: Getting Started
-
Section 2: Core Syntax and Data Types
-
Section 3: Collections
-
39: Set Operations: Union, Intersection, Difference
-
Section 4: Control Flow
-
Section 5: Functions
-
Section 6: Turtle Graphics and Early Practice Projects
-
Section 7: Working with Files and I/O
-
Section 8: Regular Expressions
-
Section 9: Object-Oriented Python
-
Section 10: Error Handling
-
Section 11: Modules and Packages
-
Section 12: Iterators, Generators, and Functional Tools
-
Section 13: Decorators and Metaprogramming
-
Section 14: Concurrency and Parallelism
-
Section 15: Working with Dates, Times, and Numbers
-
Section 16: Standard Library Deep Dive I: Data Structures
-
Section 17: Standard Library Deep Dive II: System and Introspection
-
Section 18: Standard Library Deep Dive III: Security and Encoding
-
Section 19: Standard Library Deep Dive IV: Text and Data Utilities
-
Section 20: Networking and Web Basics
-
Section 21: Working with Databases
-
Section 22: Testing and Quality
-
Section 23: Advanced Typing
-
Section 24: Context Managers and Resource Handling
-
Section 25: Text, Unicode, and Binary Data
-
Section 26: More Functional and Iteration Tools
-
Section 27: Data Validation and Configuration
-
Section 28: Working with Images and Media
-
Section 29: Property-Based and Documentation Testing
-
Section 30: Packaging and Deployment
-
Section 31: Performance and Internals
-
Section 32: Design Patterns in Python
-
Section 33: GUI Programming
-
Section 34: Security Basics
-
Section 35: Data Structures and Algorithms
-
Section 36: Practical Projects
-
Section 37: Capstone Projects
-
Section 38: Interview and Algorithm Practice
-
Section 39: Writing Idiomatic Python
369: The Builder Pattern
A few years ago, I was reviewing a pull request for a teammate who was building a complex PDF report generator. He had a Report class with a constructor that looked like a phone number: Report(title, author, footer, header, orientation, margin_top, margin_bottom, font_size, show_page_numbers, allow_links, ...). By the time he actually instantiated the object, the code looked like this: Report("Annual Audit", "Finance Team", None, True, "Portrait", 10, 10, 12, False, True).
I remember staring at that line for five minutes, trying to figure out what that False in the ninth position actually controlled. We've all been there—the "telescoping constructor" nightmare. It's fragile, impossible to read, and if you want to add one more optional parameter, you have to update every single instantiation across your entire codebase. That's where the Builder pattern saves your sanity.
The Problem with Overloaded Constructors
In Python, we have named arguments, which help a bit, but when an object requires a complex, multi-step assembly process, even kwargs can feel messy. The Builder pattern is designed to separate the construction of a complex object from its representation. Instead of one giant __init__ call, you use a separate "Builder" object that lets you configure the product piece by piece.
The real magic happens when you implement a "fluent interface." This is just a fancy way of saying that each method in your builder returns self. This allows you to chain calls together, making the code read almost like a sentence. It turns a confusing list of arguments into a clear, intentional sequence of configuration steps.
class Query:
def __init__(self):
self.table = None
self.columns = []
self.where_clause = None
def __str__(self):
cols = ", ".join(self.columns) if self.columns else "*"
query = f"SELECT {cols} FROM {self.table}"
if self.where_clause:
query += f" WHERE {self.where_clause}"
return query
class QueryBuilder:
def __init__(self):
self.query = Query()
def select(self, *columns):
self.query.columns.extend(columns)
return self # This is the key to chaining
def from_table(self, table):
self.query.table = table
return self
def where(self, condition):
self.query.where_clause = condition
return self
def build(self):
# Here you can add validation to ensure the query is valid
if not self.query.table:
raise ValueError("A table must be specified.")
return self.query
# Now, look how much cleaner this is:
builder = QueryBuilder()
sql = builder.select("id", "username", "email")\
.from_table("users")\
.where("active = 1")\
.build()
print(sql) # SELECT id, username, email FROM users WHERE active = 1
Decoupling Assembly from the Result
You might be wondering: "Why not just put these methods directly on the Query class?" In simple cases, you can. But as you scale, you'll find that the Query object should ideally be an immutable "Value Object"—something that represents a result, not something that knows how to build itself. By keeping the QueryBuilder separate, you ensure that the Query object is only ever created in a valid state via the .build() method.
I've found this particularly useful when the "build" step involves expensive operations or validation. For example, the .build() method can check if the requested columns actually exist in the table before returning the final object. If you just used a constructor, you'd be doing that validation inside __init__, which often makes the class harder to test and subclass.
When to Skip the Builder
I'll be honest: don't over-engineer this. If your class only has three or four parameters, a Builder is overkill. Python's optional arguments and keyword arguments (def __init__(self, name, age=None, city=None):) handle simple cases perfectly. Only reach for the Builder pattern when the construction process feels like a chore, or when you find yourself passing None or False into a constructor just to reach the tenth argument. If the "assembly" of the object has a logical sequence, that's your cue to build a Builder.
📋 Practical Task
Building a Custom HTTP Request Composer
You are tasked with creating a system that constructs complex HTTP requests. Some requests are simple, but others require specific headers, a JSON body, query parameters, and a specific HTTP method (GET, POST, PUT, etc.).
Your goal: Implement an HttpRequest class and an HttpRequestBuilder class. The builder should support the following chained methods:
set_method(method): Sets the HTTP method (e.g., "POST").set_url(url): Sets the destination URL.add_header(key, value): Adds a header to a dictionary of headers.set_body(body): Sets the request body.build(): Returns a finalizedHttpRequestobject.
Requirements:
- The
HttpRequestclass should be a simple data holder with a__str__method that prints the request details in a readable format. - The
build()method must raise aValueErrorif theurlormethodhas not been set. - Ensure your builder methods return
selfto allow for method chaining.
Test your implementation with this scenario: Create a POST request to "https://api.example.com/v1/login" with the header "Content-Type": "application/json" and a body of '{"username": "admin", "password": "123"}' using a single chained statement.
There are no comments for now.