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.
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.
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)artifacts/data_ingestion
Traceback (most recent call last):
File "main.py", line 5, in <module>
print(cfg.data_ingestion.root_dir)
AttributeError: 'dict' object has no attribute 'data_ingestion'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.
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))ConfigBox artifacts/data_ingestion True
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.
import json, os
from pathlib import Path
import joblib
import yaml
from box import ConfigBox
from ensure import ensure_annotationsread_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.
@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.
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"))33
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.
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")Traceback (most recent call last):
File "main.py", line 8, in <module>
get_product(2, "3")
ensure.main.EnsureError: Argument y of type <class 'str'> to <function get_product at 0x10be17240> does not match annotation type <class 'int'>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.
View the code here
TARGET_COLUMN: math_score
TEST_SIZE: 0.2
RANDOM_STATE: 42
N_ESTIMATORS: 100
MAX_DEPTH: 12
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
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
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.
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)ConfigBox artifacts/data_ingestion/stud.csv artifacts/model_trainer/metrics.json 100 0.2
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 realPathis 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.
@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.
@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_load | ConfigBox from read_yaml | |
|---|---|---|
| Reading a key | cfg["data_ingestion"]["root_dir"] | cfg.data_ingestion.root_dir |
| A missing key | KeyError | BoxKeyError, naming the attribute |
| Still a dict | Yes | Yes, it subclasses dict |
| Nested levels | One pair of brackets each | One dot each, to any depth |
| Where it comes from | The standard library | python-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
BoxKeyErrornaming the attribute instead of a bracketedKeyErroryou 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.
@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(...).Related
- Previous: Config files, not constants
- Next: Entity classes for stage config
- Reference: ConfigBox in the python-box wiki
- Call
read_yaml("config/config.yaml")with a plain string and read theEnsureErrorthat comes back. - Ask for a key that is not there,
cfg.data_ingestion.source, and compare theBoxKeyErrorwith theKeyErrora dict gives. - Remove
@ensure_annotationsfromread_yamland pass the string again, to see the helper accept it and the annotation mean nothing.
You understood something today that you didn't yesterday.