context object in data engineering is a design pattern used to reduce the number of parameters passed through function calls by grouping related dependencies and configuration settings into a single object.

This simplifies function signatures and improves maintainability, especially in complex pipelines or ETL applications, from a Data Engineering perspective.

A great example of a context object design is Airflow’s context object, which includes details about task_instance, dag_runs, dagconf, etc.

Here’s a simple example of how this can be used in an ETL pipeline

Before Context Object

def extract_data(spark_session, logger, source_path):
    logger.info("Extracting data")
    df = spark_session.read.parquet(source_path)
    return df
 
def transform_data(logger, config, df):
    logger.info("Transforming data")
    columns = config.get("selected_columns", [])
    if columns:
        df = df.select(*columns)
    return df
 
def load_data(logger, df, target_path):
    logger.info("Loading data")
    df.write.mode("overwrite").parquet(target_path)
 
# Usage:
df = extract_data(my_spark_session, my_logger, "s3://input/")
df_transformed = transform_data(my_logger, {"selected_columns": ["id", "timestamp", "value"]}, df)
load_data(my_logger, df_transformed, "s3://output/")
 

Each function needs many parameters; calling each function is verbose and error-prone if you have to pass multiple configs/resources repeatedly.

After Using a Context Object

Context object can be defined using Dataclasses.

from dataclasses import dataclass
from pyspark.sql import SparkSession
from custom_logging import SparkLogger
 
@dataclass
class ETLContext:
    spark_session: SparkSession
    logger: SparkLogger
    config: dict
 
def extract_data(context: ETLContext, source_path: str):
    context.logger.info("Extracting data")
    df = context.spark_session.read.parquet(source_path)
    return df
 
def transform_data(context: ETLContext, df):
    context.logger.info("Transforming data")
    columns = context.config.get("selected_columns", [])
    if columns:
        df = df.select(*columns)
    return df
 
def load_data(context: ETLContext, df, target_path: str):
    context.logger.info("Loading data")
    df.write.mode("overwrite").parquet(target_path)
 
 
# Usage:
etl_context = ETLContext(
    spark_session=my_spark_session,
    logger=my_logger,
    config={"selected_columns": ["id", "timestamp", "value"]}
)
 
df = extract_data(etl_context, "s3://input/")
df_transformed = transform_data(etl_context, df)
load_data(etl_context, df_transformed, "s3://output/")

This reduces repetition, function signatures are cleaner, and adding new dependencies (e.g., user credentials) only requires updating the Context object, not all function signatures.

Warning

Context objects should be used judiciously, especially in higher-level orchestration; avoid overuse in low-level utility functions to minimize coupling