*Published on 11/16/2025*
# Context object pattern
I recently wrote about a great creational design pattern, the [[builder method design pattern|builder pattern]]. It’s fantastic for cleaning up messy object construction, especially when you’ve got tons of optional keyword arguments that make your `__init__` look like a Walmart receipt.
But what about _functions_?
When you’re scripting, orchestrating a pipeline, or calling multiple functions in a sequence, writing a builder just to group parameters feels like overkill. And unlike object creation, what you really want is a clean, consistent way to pass shared metadata or configuration through each step.
So what options do we have — besides crying into our keyboards?
### The Context Object Pattern
The context object pattern says:
> Instead of passing ten separate keyword arguments to every function, create **one object** that carries all the shared state.
This is particularly useful when:
- Multiple parameters naturally belong together as "a thing"
- You want type validation or structure around what gets passed
- You’re tired of updating 12 function signatures every time a requirement changes
Let’s walk through an example.
### Chad the Data Engineer and the Pipeline of Many Arguments
Chad just wanted to write a simple pipeline. But as he coded, he kept thinking: _"Wait, I just need one more parameter…"_ And one became two. Then seven. Then it spiraled into this:
```python
def run_pipeline(
data_path,
logger,
metrics,
user_id,
run_id,
retry_count,
alert_email,
db_client,
region,
is_test_run,
):
logger.info(f"Running pipeline for {user_id} in {region}")
metrics.increment("pipeline.start")
data = extract_data(
data_path,
logger,
metrics,
user_id,
run_id,
retry_count,
alert_email,
db_client,
region,
is_test_run,
)
transformed = transform_data(
data,
logger,
metrics,
user_id,
run_id,
retry_count,
alert_email,
db_client,
region,
is_test_run,
)
load_data(
transformed,
logger,
metrics,
user_id,
run_id,
retry_count,
alert_email,
db_client,
region,
is_test_run,
)
```
This is… a lot. Hard to read, painful to maintain, and guaranteed to break the moment you add one more parameter.
And you’ll notice: _all the same metadata is passed repeatedly._ Exactly the sort of thing a context object cleans up.
### Enter the Context Object
Here’s how we clean it up: define a single object containing all the shared context.
```python
from dataclasses import dataclass
@dataclass
class PipelineContext:
data_path: str
logger: object
metrics: object
user_id: str
run_id: str
retry_count: int
alert_email: str
db_client: object
region: str
is_test_run: bool
```
Now your functions take **one** parameter instead of eleven. And your pipeline suddenly becomes readable again.
```python
def extract_data(context: PipelineContext):
context.logger.info(f"Extracting for user {context.user_id}")
return context.db_client.read(context.data_path)
def transform_data(context: PipelineContext, data):
context.metrics.increment("transform.start")
return [row for row in data if row["region"] == context.region]
def load_data(context: PipelineContext, data):
context.logger.info(f"Loading {len(data)} rows")
context.db_client.write("output", data)
def run_pipeline(context: PipelineContext):
context.logger.info("Pipeline starting")
context.metrics.increment("pipeline.start")
data = extract_data(context)
transformed = transform_data(context, data)
load_data(context, transformed)
```
Take a breath. Your eyes have stopped bleeding.
### Why This Pattern Rocks
Because the pipeline context is defined in one place:
- **Your code is more maintainable.** Add a new parameter? Update one class, not ten signatures.
- **Debugging gets easier.** There’s _one_ object to inspect, not a scattered mess of arguments.
- **It scales naturally.** More pipelines? Reuse or extend your context class.
This pattern also shines for things like configuration, shared metadata, runtime state, loggers, clients, caches, and anything else you want in one bundle. Popular libraries use it too: Django passes around the `request` object for a reason.
### A Quick Word of Caution
Like any pattern, this one can be abused.
If your context object grows into a junk drawer of unrelated fields ("Yes, this object contains region, a logger, and also a list of coupons for some reason"), you’ve recreated the same problem, just in a different shape.
The goal is to define **coherent context** that logically belongs together — not to shove everything into one object "just because we can."
### In Short
Don't be like Chad. Write better code with the context object pattern.
As always, you can find the code examples at my [jacobwritescode repo](https://github.com/mrjaketomlinson/jacobwritescode)!
Happy coding! 😁