A lightweight, zero-dependency config loader for Python data-processing pipelines.
- Walks parent directories automatically to find
config.json,config.yaml, orconfig.toml - Loads
.envfrom the same directory intoos.environ(without overwriting existing vars) - Auto-detects the calling script's name as the active section — no boilerplate
- Special
_globalssection — its values are merged into every section automatically - Resolves
{{section.key.subkey}}cross-references at arbitrary depth - Interpolates
${ENV_VAR}placeholders fromos.environin string values - Attribute-style access with IDE tab-completion (
cfg.lr,cfg.paths.raw) - Clear error messages when a reference can't be resolved
pip install refconf-managerWith YAML support:
pip install "refconf-manager[yaml]"project/
├── config/
│ ├── config.json
│ └── .env
├── preprocess.py
└── pipeline/
└── train.py ← ConfigManager() walks up and finds config/config.json
config/config.json
{
"_globals": {
"root": "data/",
"version": "v2"
},
"preprocess": {
"output": "{{root}}clean.csv"
},
"train": {
"input": "{{preprocess.output}}",
"lr": 0.01,
"run_id": "{{version}}"
}
}train.py
from refconf_manager import ConfigManager
cfg = ConfigManager() # section="train" detected from filename
cfg["input"] # → "data/clean.csv" (resolved via preprocess)
cfg.input # → "data/clean.csv" (attribute-style shorthand)
cfg["lr"] # → 0.01
cfg.lr # → 0.01
cfg["version"] # → "v2" (injected from _globals)
cfg["run_id"] # → "v2"
cfg.get("device", "cpu") # → "cpu" (default)Each script reads only its own section. {{section.key}} wires outputs to inputs.
# preprocess.py
cfg = ConfigManager()
df.to_csv(cfg["output"]) # saves to "data/clean.csv"
# train.py
cfg = ConfigManager()
df = pd.read_csv(cfg["input"]) # reads "data/clean.csv" automaticallyPut anything shared (paths, versions, DB hosts) in _globals and access it directly in every section without any {{...}} syntax.
{
"_globals": {
"db_host": "localhost",
"root": "data/"
},
"etl": { "source": "{{root}}raw.csv" },
"api": { "host": "{{db_host}}" }
}cfg = ConfigManager()
cfg["db_host"] # available in every sectionReference values at any depth using dot notation.
{
"infra": {
"storage": { "bucket": "my-bucket", "prefix": "runs/" }
},
"train": {
"output": "{{infra.storage.bucket}}/{{infra.storage.prefix}}model.pt"
}
}cfg = ConfigManager()
cfg["output"] # → "my-bucket/runs/model.pt"When a config value is itself a dict, attribute access returns a namespace object that supports the same interface — including further attribute access, __dir__, len(), and iteration.
{
"train": {
"db": { "host": "localhost", "port": 5432 }
}
}cfg = ConfigManager()
cfg.db.host # → "localhost"
cfg.db.port # → 5432
cfg.db == {"host": "localhost", "port": 5432} # → True
cfg.db.to_dict() # → {"host": "localhost", "port": 5432}Use ${VAR} in any string value. Variables from the .env file are available too.
{
"app": {
"db_url": "postgresql://${DB_USER}:${DB_PASS}@${DB_HOST}/mydb"
}
}# config/.env
DB_HOST=localhost
DB_USER=admin
DB_PASS=secret
cfg = ConfigManager()
cfg["db_url"] # → "postgresql://admin:secret@localhost/mydb"Pass your own logger to capture config loading events at DEBUG level.
import logging
from refconf_manager import ConfigManager
logging.basicConfig(level=logging.DEBUG)
log = logging.getLogger(__name__)
cfg = ConfigManager(logger=log)
# DEBUG config_manager: Config file: /project/config.json
# DEBUG config_manager: Active section: 'train' | globals: ['root', 'version'] | ...# Load a specific section explicitly
cfg = ConfigManager(section="shared")
# Start searching from a different directory
cfg = ConfigManager(start_dir="/path/to/project")When a reference can't be resolved, you get a clear, actionable message:
KeyError: "Reference {{preprocess.no_such_key}}: Key 'no_such_key' not found
under 'preprocess'. Available: ['output', 'batch_size']"
KeyError: "Reference {{version}}: no section prefix given and 'version' not
found in '_globals'. Use {{section.version}} or add it to '_globals'."
Config files must live inside a config/ directory.
The .env file also goes in the same config/ directory.
project/
├── config/
│ ├── config.json ← or config.yaml / config.toml
│ └── .env
└── scripts/
└── train.py
ConfigManager() walks up from the calling script until it finds config/config.json (or yaml/toml).
| File | Notes |
|---|---|
config/config.json |
No extra dependencies |
config/config.yaml / config.yml |
Requires pip install pyyaml |
config/config.toml |
Built-in on Python 3.11+; pip install tomli on older |
Use {{section.key}} or {{section.key.subkey.deeper}} in any string value:
{
"_globals": {"root": "data/"},
"shared": {"db": {"host": "localhost", "port": 5432}},
"app": {
"db_url": "postgresql://{{shared.db.host}}:{{shared.db.port}}/mydb",
"data_dir": "{{root}}processed/"
}
}References work inside nested dicts and lists too.
Bare {{key}} (no dot) is resolved from _globals.
Use ${VAR} to interpolate os.environ values in any string. Substitution happens after {{}} reference resolution, so both syntaxes can coexist:
{
"app": {
"host": "${DB_HOST}",
"db_url": "postgresql://${DB_USER}@${DB_HOST}/{{_globals.db_name}}"
}
}Variables set in config/.env are loaded before interpolation, so they are available as ${VAR} too. A missing variable raises KeyError with a clear message.
| Parameter | Type | Default | Description |
|---|---|---|---|
section |
str |
auto-detected | Config section to load |
start_dir |
str | Path |
caller's directory | Where to start searching |
logger |
logging.Logger |
None |
Logger for internal debug events |
| Property | Returns | Description |
|---|---|---|
section |
str |
Active section name |
config_path |
Path |
Resolved path to the config file |
cfg["key"] # dict-style access (KeyError if missing)
cfg.key # attribute-style shorthand (AttributeError if missing)
cfg.get("key", default=None) # with default
"key" in cfg # membership test
len(cfg) # number of keys in the active section
for k in cfg: ... # iterate over keys
cfg.keys() / .values() / .items()
cfg.to_dict() # plain dict (recursively unwraps nested namespaces)Attribute-style access also enables IDE tab-completion: type cfg. and your editor
will suggest the keys loaded from the active section (Pylance, Jedi, IPython, Jupyter).
Nested dict values return a _Namespace object that supports the same interface,
so cfg.db.host and dir(cfg.db) both work.
Note: if a config key shares a name with a built-in method (e.g.
get,keys), the method wins on attribute access. Usecfg["get"]in that case.
Note:
ConfigManageris read-only — assigning to any public attribute raisesAttributeError.
git clone https://github.com/lukaszplk/config-manager
cd config-manager
pip install -e ".[dev]"
pytestThe PyPI package is
refconf-manager; the import name isrefconf_manager.