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 →

Reading config with ConfigBox

ConfigBox is a dictionary type from the python-box package whose keys can also be read as attributes, so stage code written against a parsed YAML file says cfg.data_ingestion.root_dir instead of cfg["data_ingestion"]["root_dir"].

Last updated: 19 Oct, 2026 · MLOps

Config files, not constants left the three YAML files parsed into nested dictionaries and read with brackets. Every stage is about to read from them, so the project wraps the loading in one helper, read_yaml, which returns a ConfigBox and so gives every key a dotted name. The same helper file carries the other small jobs each stage repeats: making folders and saving a metrics file or a fitted object.

Syntax: read_yaml(path: Path) -> ConfigBox takes the path of a YAML file and returns its contents with dotted access. The versions in use here are python-box 7.4.1 and ensure 1.0.4.

A dict raises AttributeError, a ConfigBox does not · from the Complete End to End Deep Learning Project With MLFLOW,DVC And Deployment · 51:23 to 55:36

The clip reads the deep-learning project's config in a notebook; this lesson reads the student-performance config through the same helper, and ConfigBox behaves the same either way.

Reading a nested key as an attribute

A dictionary from yaml.safe_load supports brackets and no other way in. Reading the same key with a dot fails, and the error names what is missing.

ExampleA plain dict has no attribute for its key
cfg = {"data_ingestion": {"root_dir": "artifacts/data_ingestion"}}
print(cfg["data_ingestion"]["root_dir"])

# the same key read as an attribute, the way stage code would prefer to write it
print(cfg.data_ingestion.root_dir)

The message is exact: a dict has no attribute called data_ingestion, so the read fails at the first dot. With three nested files and four stages, the alternative is a quoted string for every setting read, each one a typo away from a KeyError at run time.

Reading the same keys with dots

ConfigBox takes the dictionary and returns a view of it where a key is also an attribute. It is still a dictionary, so brackets keep working and anything expecting a mapping is happy.

ExampleThe same dictionary, read with dots
from box import ConfigBox

cfg = ConfigBox({"data_ingestion": {"root_dir": "artifacts/data_ingestion"}})

print(type(cfg).__name__)
print(cfg.data_ingestion.root_dir)
print(isinstance(cfg, dict))

Loading YAML through one helper

Rather than let each stage import yaml and remember to wrap the result, the project keeps one shared module of small helpers. These are its imports.

python
import json, os
from pathlib import Path

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

read_yaml is three steps: open the path, parse it, wrap the result. The annotations say what goes in and what comes out, and the decorator above them is what makes those annotations bite.

python
@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))

Catching a wrong argument type with ensure

A Python annotation is a note for a reader and a type checker. At run time nothing enforces it, so a function annotated for integers accepts a string and does something surprising with it.

ExampleAn annotation nothing enforces
def get_product(x: int, y: int) -> int:
    return x * y

# y is annotated int, so what does a str do here?
print(get_product(2, "3"))

The call returned 33: Python multiplied the string "3" by 2 and repeated it. No error, a wrong answer, and a bug that surfaces much later as a strange number in a metrics file. The @ensure_annotations decorator from the ensure package checks each argument against its annotation as the call is made.

Exampleensure_annotations turns the annotation into a real check
from ensure import ensure_annotations

@ensure_annotations
def get_product(x: int, y: int) -> int:
    return x * y

# the same wrong call, now refused at the door
get_product(2, "3")

EnsureError names the argument, the type that arrived and the type the annotation asked for. That is why every helper in the shared module carries the decorator: read_yaml wants a Path, and a stage that hands it a string is told so at the call rather than three stages later.

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/__init__.py

Reading config.yaml with read_yaml

With the helper in place, a stage loads a file and walks it with dots. Run this from the project root.

ExampleThe project's own config, read with dots
from pathlib import Path

from sp.utils.common import read_yaml

# a Path, not a string: the annotation on read_yaml is checked
cfg = read_yaml(Path("config/config.yaml"))
params = read_yaml(Path("params.yaml"))

print(type(cfg).__name__)
print(cfg.data_ingestion.local_data_file)
print(cfg.model_trainer.metrics_file)
print(params.N_ESTIMATORS, params.TEST_SIZE)

What the dotted reads gave back

  • The return type is ConfigBox, so every nested key below it is reachable with a dot without any further wrapping.
  • A path came back as the string in the file, artifacts/data_ingestion/stud.csv, which is still text: turning it into a real Path is a separate job, and the lesson on Entity classes for stage config is where it happens.
  • params.yaml came back with its number types intact, 100 as an int and 0.2 as a float, so a hyperparameter can go straight into a model constructor.
  • One call per file, which matters because the configuration manager makes exactly three of these calls for a whole training run.

The other jobs every stage repeats

Two more helpers live in the same module. create_directories makes a stage's output folder before the stage writes into it, and takes a list because one stage can own several.

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

exist_ok=True is what makes a second run harmless. The last two helpers save the things a run produces: a small JSON report, and a fitted Python object such as a preprocessor or a model.

python
@ensure_annotations
def save_json(path: Path, data: dict):
    with open(path, "w") as f:
        json.dump(data, f, indent=4)      # indented, so a human can read the metrics file


@ensure_annotations
def save_object(path: Path, obj):
    joblib.dump(obj, path)                # a fitted preprocessor or model


@ensure_annotations
def load_object(path: Path):
    return joblib.load(path)

A plain dict against a ConfigBox

dict from yaml.safe_loadConfigBox from read_yaml
Reading a keycfg["data_ingestion"]["root_dir"]cfg.data_ingestion.root_dir
A missing keyKeyErrorBoxKeyError, naming the attribute
Still a dictYesYes, it subclasses dict
Nested levelsOne pair of brackets eachOne dot each, to any depth
Where it comes fromThe standard librarypython-box, pinned in requirements.txt

Where the shared helpers pay off

  • Four stages reading two files each, where one helper means one place to change how loading works, and one place where a bad path is reported.
  • A typo in a key, which raises a BoxKeyError naming the attribute instead of a bracketed KeyError you have to trace back to a quoted string.
  • Saving a metrics file, where every stage writes JSON the same way, so a pipeline tool and a human reader both find what they expect.
Watch out. @ensure_annotations only checks arguments that carry an annotation, so an unannotated parameter is unchecked and a helper written without annotations gains nothing from the decorator. It also means the annotation is now part of the contract: changing path: Path to path: str changes which callers are allowed, which is why the manager wraps its paths in Path(...).
Try it yourself
  • Call read_yaml("config/config.yaml") with a plain string and read the EnsureError that comes back.
  • Ask for a key that is not there, cfg.data_ingestion.source, and compare the BoxKeyError with the KeyError a dict gives.
  • Remove @ensure_annotations from read_yaml and pass the string again, to see the helper accept it and the annotation mean nothing.

You understood something today that you didn't yesterday.