MLOpsThe MLOps lifecycle · MLflow 3.17 · DVC 3.67 · FastAPI · Docker · GitHub Actions · Evidently · Python 3.11+
Dashboard
0%
1
Curious builder0 XP earned · 300 to level 2
0 daysFinish a lesson to begin
Badge collection0 of 6 unlocked
50 small wins to finish your pathNext lesson →

The configuration manager

The configuration manager is the one class that reads config.yaml, params.yaml and schema.yaml at start-up and hands each stage its own entity object, creating the folder that stage writes into as it goes.

Last updated: 19 Oct, 2026 · MLOps

Entity classes for stage config declared what a stage's settings look like, and built one by hand. Something has to build them from the files for real, and it should not be the stages: four stages each opening YAML is four places to get a path wrong. The manager is the single seam between the files and the typed objects, and it is the next file in the video's workflow order.

One class that reads both YAML files and hands out typed config · from the Complete End to End Deep Learning Project With MLFLOW,DVC And Deployment · 1:19:03 to 1:23:03

The clip's manager reads config.yaml and params.yaml; this one reads a third file, schema.yaml, because the student-performance pipeline validates its columns before it trains.

Reading the three files once

The imports name the four entity classes it builds and the two helpers it uses.

python
from pathlib import Path    # every path a stage receives is a Path, not text

from sp.entity.config_entity import (
    DataIngestionConfig,
    DataTransformationConfig,
    DataValidationConfig,
    ModelTrainerConfig,
)
from sp.utils.common import create_directories, read_yaml

The constructor takes the three file paths as defaults, so a test can point it at a different set of files, and reads each one into a ConfigBox.

python
class ConfigurationManager:
    """Reads the YAML files once, then hands each stage its own typed config."""

    def __init__(
        self,
        config_path=Path("config/config.yaml"),
        params_path=Path("params.yaml"),
        schema_path=Path("schema.yaml"),
    ):
        # read once at start-up: no stage opens a YAML file itself
        self.config = read_yaml(config_path)
        self.params = read_yaml(params_path)
        self.schema = read_yaml(schema_path)

Handing data ingestion its config

There is one getter per stage, and each one does three things: pick that stage's block out of config.yaml, make the folder, and return a filled entity. The return annotation says which entity, so a getter that returns the wrong shape is obvious.

python
    def get_data_ingestion_config(self) -> DataIngestionConfig:
        c = self.config.data_ingestion
        create_directories([c.root_dir])        # the stage can write the moment it starts

        return DataIngestionConfig(
            root_dir=Path(c.root_dir),
            source_url=c.source_url,
            local_data_file=Path(c.local_data_file),
            train_file=Path(c.train_file),
            test_file=Path(c.test_file),
            test_size=self.params.TEST_SIZE,    # wiring from config, numbers from params
            random_state=self.params.RANDOM_STATE,
        )

This is where the two files meet. The paths come from the stage's block in config.yaml, wrapped in Path on the way through; the split size and the seed come from params.yaml. The stage never learns that there were two files.

Giving validation the schema

The validation getter is the only one that touches the third file. It passes the columns as a plain dict, because that is what the entity's annotation asks for and @ensure_annotations is checking.

python
    def get_data_validation_config(self) -> DataValidationConfig:
        c = self.config.data_validation
        create_directories([c.root_dir])

        return DataValidationConfig(
            root_dir=Path(c.root_dir),
            status_file=Path(c.status_file),
            expected_columns=dict(self.schema.COLUMNS),   # a plain dict, as the entity asks
        )

The transformation getter is the shortest: a folder, the file the fitted preprocessor is saved to, and the name of the column being predicted.

python
    def get_data_transformation_config(self) -> DataTransformationConfig:
        c = self.config.data_transformation
        create_directories([c.root_dir])

        return DataTransformationConfig(
            root_dir=Path(c.root_dir),
            preprocessor_file=Path(c.preprocessor_file),
            target_column=self.params.TARGET_COLUMN,      # which column the model predicts
        )

Passing the hyperparameters to the trainer

The trainer's getter reaches furthest into params.yaml: the target column, both forest settings and the seed.

python
    def get_model_trainer_config(self) -> ModelTrainerConfig:
        c = self.config.model_trainer
        create_directories([c.root_dir])

        return ModelTrainerConfig(
            root_dir=Path(c.root_dir),
            model_file=Path(c.model_file),
            metrics_file=Path(c.metrics_file),
            target_column=self.params.TARGET_COLUMN,
            n_estimators=self.params.N_ESTIMATORS,        # tune these in params.yaml
            max_depth=self.params.MAX_DEPTH,
            random_state=self.params.RANDOM_STATE,        # the same seed on every run
        )
Project files used on this pageThis lesson builds on a project from earlier lessons. The code below imports these files. Click a file to see its code, or follow the link to the lesson that wrote it. To run the code yourself, keep them in the same folder.
View the code here
params.yaml
TARGET_COLUMN: math_score
TEST_SIZE: 0.2
RANDOM_STATE: 42
N_ESTIMATORS: 100
MAX_DEPTH: 12
schema.yaml
COLUMNS:
  gender: object
  race_ethnicity: object
  parental_level_of_education: object
  lunch: object
  test_preparation_course: object
  math_score: int64
  reading_score: int64
  writing_score: int64
sp/__init__.py
config/config.yaml
data_ingestion:
  root_dir: artifacts/data_ingestion
  source_url: https://raw.githubusercontent.com/krishnaik06/mlproject/main/notebook/data/stud.csv
  local_data_file: artifacts/data_ingestion/stud.csv
  train_file: artifacts/data_ingestion/train.csv
  test_file: artifacts/data_ingestion/test.csv

data_validation:
  root_dir: artifacts/data_validation
  status_file: artifacts/data_validation/status.json

data_transformation:
  root_dir: artifacts/data_transformation
  preprocessor_file: artifacts/data_transformation/preprocessor.joblib

model_trainer:
  root_dir: artifacts/model_trainer
  model_file: artifacts/model_trainer/model.joblib
  metrics_file: artifacts/model_trainer/metrics.json
sp/utils/common.py
import json, os
from pathlib import Path

import joblib
import yaml
from box import ConfigBox
from ensure import ensure_annotations


@ensure_annotations
def read_yaml(path: Path) -> ConfigBox:
    """Load a YAML file so its keys can be read with dots, not brackets."""
    with open(path) as f:
        return ConfigBox(yaml.safe_load(f))


@ensure_annotations
def create_directories(paths: list):
    for p in paths:
        os.makedirs(p, exist_ok=True)


@ensure_annotations
def save_json(path: Path, data: dict):
    with open(path, "w") as f:
        json.dump(data, f, indent=4)


@ensure_annotations
def save_object(path: Path, obj):
    joblib.dump(obj, path)


@ensure_annotations
def load_object(path: Path):
    return joblib.load(path)
sp/utils/__init__.py
sp/config/__init__.py
sp/entity/__init__.py
sp/entity/config_entity.py
from dataclasses import dataclass
from pathlib import Path


@dataclass(frozen=True)
class DataIngestionConfig:
    root_dir: Path
    source_url: str
    local_data_file: Path
    train_file: Path
    test_file: Path
    test_size: float
    random_state: int


@dataclass(frozen=True)
class DataValidationConfig:
    root_dir: Path
    status_file: Path
    expected_columns: dict


@dataclass(frozen=True)
class DataTransformationConfig:
    root_dir: Path
    preprocessor_file: Path
    target_column: str


@dataclass(frozen=True)
class ModelTrainerConfig:
    root_dir: Path
    model_file: Path
    metrics_file: Path
    target_column: str
    n_estimators: int
    max_depth: int
    random_state: int

Asking the manager for two stages' settings

One manager, built once, answers for every stage. Run this from the project root, where the three YAML files are.

ExampleTwo stages' settings, built from the files
import os

from sp.config.configuration import ConfigurationManager

manager = ConfigurationManager()      # the three files are read here, once

ingestion = manager.get_data_ingestion_config()
trainer = manager.get_model_trainer_config()

print("ingestion writes to:", ingestion.root_dir)
print("split and seed     :", ingestion.test_size, ingestion.random_state)
print("forest             :", trainer.n_estimators, trainer.max_depth)
print("target column      :", trainer.target_column)
print("folders now on disk:", sorted(os.listdir("artifacts")))

What the manager produced

  • Each stage got only its own block: the ingestion object knows about artifacts/data_ingestion and nothing about the trainer's files.
  • The numbers arrived from params.yaml, the 0.2 split and the seed 42 on the ingestion object, 100 trees and a depth of 12 on the trainer's, with no number typed in Python.
  • The target column is a setting too, math_score, so the trainer and the transformer agree on what is being predicted without either one hard-coding it.
  • Two folders exist that did not before, because each getter called create_directories before returning; asking for all four settings creates all four folders.

YAML in each stage against one manager

Each stage reads YAMLOne configuration manager
Files opened per runTwo or three per stageThree, once
Where a path is wrapped in PathIn every stageIn one getter
Who makes the output folderEach stage, or nobodyThe getter, before the stage runs
Testing a stageNeeds YAML files on diskHand it an entity you built yourself
Renaming a key in config.yamlEvery stage that read itThe one getter that reads it

Where one manager pays off

  • A renamed key, where source_url becoming source is a one-line change in one getter rather than a search across the stages.
  • Unit testing a stage, where you build its entity by hand and never put a YAML file in the test folder at all.
  • A second environment, where pointing the manager at another config.yaml moves every output folder at once.
Watch out. The create_directories call belongs in the getter, not in the stage. Forget it in a new getter and the stage fails on its first write with a FileNotFoundError for a folder nobody created, which reads like a bug in the stage and is a missing line here.
Try it yourself
  • Call all four getters and print sorted(os.listdir("artifacts")) again to see four folders instead of two.
  • Point the manager at a copy of the config file, ConfigurationManager(config_path=Path("config/other.yaml")), with a different root_dir, and watch the other folder appear.
  • Delete the create_directories line from the trainer's getter and see which error the trainer gives when it tries to save.

This is what real progress feels like.