π Python Data Workflows β βοΈ Configuration-Driven Pipelines π
Posted on: October 1, 2026
Description:
As data workflows grow, hardcoded values quickly become difficult to maintain.
A script may contain file paths, validation rules, column names, thresholds, and output locations directly inside the Python code.
That works initially, but every change requires editing the script.
A configuration-driven pipeline solves this by separating what the workflow should do from how the workflow works.
What Is a Configuration-Driven Pipeline?
Instead of writing settings like this:
df = pd.read_csv(
"sales_data.csv"
)
the pipeline reads them from a configuration file:
df = pd.read_csv(
config["input_file"]
)
The Python logic stays unchanged while the configuration controls its behavior.
Using JSON for Configuration
JSON is a simple option for storing pipeline settings.
{
"input_file": "sales_data.csv",
"minimum_sales": 0,
"group_by": "category"
}
The file can contain:
- source paths
- output paths
- required columns
- validation thresholds
- grouping fields
- processing options
Loading Configuration in Python
Python's built-in json module can load the settings.
with open(
"config.json",
"r",
encoding="utf-8"
) as file:
config = json.load(file)
After that, values can be accessed anywhere in the workflow.
Configurable Validation
Even validation rules can be externalised.
required_columns = set(
config["required_columns"]
)
If another dataset requires different fields, only the JSON file needs to change.
Configurable Transformations
The same idea works for transformations.
for column in config[
"numeric_columns"
]:
df[column] = pd.to_numeric(
df[column],
errors="coerce"
)
This makes the pipeline more flexible than hardcoding sales, quantity, or other fields throughout the script.
Why This Matters
Configuration-driven design makes workflows:
- easier to reuse
- easier to maintain
- safer to change
- simpler to deploy across environments
- less dependent on hardcoded values
For example, development and production pipelines can use different configuration files while sharing the exact same Python code.
JSON vs YAML
The same concept can also be implemented using YAML.
JSON has the advantage of built-in Python support, while YAML is often easier for humans to read when configuration files become larger.
Regardless of the format, the important idea is keeping configuration separate from pipeline logic.
Key Takeaways
- Configuration separates settings from code
- JSON provides a simple way to control pipeline behaviour
- Validation and transformation rules can also be configurable
- Reusable workflows require fewer code changes
Code Snippet:
import json
from pathlib import Path
import pandas as pd
CONFIG_FILE = Path("config.json")
def load_config(config_file: Path) -> dict:
"""Load pipeline configuration from JSON."""
if not config_file.exists():
raise FileNotFoundError(f"Configuration file not found: {config_file}")
with open(config_file, "r", encoding="utf-8") as file:
config = json.load(file)
print(f"β
Configuration loaded: {config_file.name}")
return config
def validate_config(config: dict) -> None:
"""Validate required configuration keys."""
required_keys = {
"input_file",
"output_file",
"report_file",
"required_columns",
"numeric_columns",
"drop_duplicates_on",
"minimum_sales",
"minimum_quantity",
"group_by",
}
missing_keys = required_keys - set(config.keys())
if missing_keys:
raise ValueError(f"Missing configuration keys: {sorted(missing_keys)}")
print("β
Configuration validated")
def extract_data(config: dict) -> pd.DataFrame:
"""Load data using the configured input path."""
input_file = Path(config["input_file"])
if not input_file.exists():
raise FileNotFoundError(f"Input file not found: {input_file}")
df = pd.read_csv(input_file)
print(f"β
Loaded {len(df)} records from {input_file.name}")
return df
def validate_data(df: pd.DataFrame, config: dict) -> None:
"""Validate dataset using configured rules."""
required_columns = set(config["required_columns"])
missing_columns = required_columns - set(df.columns)
if missing_columns:
raise ValueError(f"Missing required columns: {sorted(missing_columns)}")
duplicate_column = config["drop_duplicates_on"]
if duplicate_column not in df.columns:
raise ValueError(f"Duplicate-check column '{duplicate_column}' was not found.")
group_by = config["group_by"]
if group_by not in df.columns:
raise ValueError(f"Grouping column '{group_by}' was not found.")
print("β
Dataset validation passed")
def transform_data(df: pd.DataFrame, config: dict) -> pd.DataFrame:
"""Apply configurable cleaning rules."""
transformed_df = df.copy()
for column in config["numeric_columns"]:
if column not in transformed_df.columns:
raise ValueError(f"Numeric column '{column}' was not found.")
transformed_df[column] = pd.to_numeric(transformed_df[column], errors="coerce")
transformed_df = transformed_df.drop_duplicates(subset=[config["drop_duplicates_on"]], keep="first")
transformed_df = transformed_df.dropna(subset=config["numeric_columns"])
transformed_df = transformed_df[
(transformed_df["sales"] >= config["minimum_sales"])
& (transformed_df["quantity"] >= config["minimum_quantity"])
].copy()
transformed_df["revenue_per_item"] = (transformed_df["sales"] / transformed_df["quantity"]).round(2)
print(f"β
Transformation completed | records={len(transformed_df)}")
return transformed_df
def generate_report(df: pd.DataFrame, config: dict) -> pd.DataFrame:
"""Generate a summary using configured grouping."""
group_by = config["group_by"]
summary = (
df.groupby(group_by, observed=True)
.agg(
total_sales=("sales", "sum"),
average_sales=("sales", "mean"),
total_quantity=("quantity", "sum"),
total_orders=("order_id", "count"),
)
.reset_index()
.sort_values("total_sales", ascending=False)
)
summary["average_sales"] = summary["average_sales"].round(2)
print(f"β
Report generated | group_by={group_by}")
return summary
def load_outputs(df: pd.DataFrame, summary: pd.DataFrame, config: dict) -> None:
"""Save configured pipeline outputs."""
output_file = Path(config["output_file"])
report_file = Path(config["report_file"])
output_file.parent.mkdir(parents=True, exist_ok=True)
report_file.parent.mkdir(parents=True, exist_ok=True)
df.to_csv(output_file, index=False)
summary.to_csv(report_file, index=False)
print(f"πΎ Processed data saved: {output_file}")
print(f"πΎ Summary saved: {report_file}")
def display_summary(df: pd.DataFrame, config: dict) -> None:
"""Display a concise workflow summary."""
print("\nπ Pipeline Summary")
print(f"Processed records: {len(df)}")
print(f"Total sales: {df['sales'].sum():,.2f}")
print(f"Grouping field: {config['group_by']}")
def run_pipeline() -> None:
"""Run the configuration-driven pipeline."""
print("π Starting Configuration-Driven Pipeline\n")
try:
config = load_config(CONFIG_FILE)
validate_config(config)
raw_df = extract_data(config)
validate_data(raw_df, config)
processed_df = transform_data(raw_df, config)
if processed_df.empty:
raise ValueError("No valid records remain after transformation.")
summary = generate_report(processed_df, config)
load_outputs(processed_df, summary, config)
display_summary(processed_df, config)
print("\nβ
Configuration-driven pipeline completed successfully")
except (FileNotFoundError, ValueError, KeyError, json.JSONDecodeError, pd.errors.ParserError) as error:
print(f"\nβ Pipeline failed: {error}")
if __name__ == "__main__":
run_pipeline()
No comments yet. Be the first to comment!