Skip to content
Boris Novichkov
ResumeGitHub ↗
ENRU
All work

bestconfig

One Config object over every place a Python project hides its settings

A configuration library that removes the setup step entirely. Instantiate Config() and it walks up from the current file to the project root, finds the YAML, JSON, INI, .env and Python config files along the way, folds in environment variables, and exposes the result through one typed accessor.

Shipped
Role
Author
Period
Mar 2021 — Apr 2023
python
$ pip install bestconfig # app/main.py — config.yaml and .env live two levels upfrom bestconfig import Config config = Config("myconfig.json")# that was the entire configuration step mode = config.logger.mode        # 'WARNING'port = config.int('PORT')        # 5050pw   = config.get('DATABASE_PASSWORD')config.to_dict()  ->  every source, merged

The whole setup step, taken from the project README.

The problem

Every Python project reinvents the same fifty lines: find the config file, decide whether the environment overrides it, parse whichever formats this project happens to use, cast the strings, and produce a friendly error when something is missing. It is not hard work, and it is never the same twice.

bestconfig deletes the step. Config() scans upward from the calling file to the project root, picks up every recognised config file it passes, folds in the environment, and hands back a single object.

What I wanted from the API

No paths. A file in app/main.py should find config.yaml at the repository root without being told where it is. Discovery walks up, so moving a module around does not break its configuration.

Defaults matching what people actually name things. Every combination of config, configuration, settings, setting and conf with .json, .yaml, .ini, .env and .cfg, plus the specific names .env, env_file and config.py. Most projects pass no arguments at all.

Missing values behave differently depending on how you ask. config['key'] raises, config.get('key') returns None, config.get('key', default) returns the default, and config.assert_contains('key') fails fast at startup. The choice of accessor is the choice of failure mode — that is the point.

Types resolved once, at the edge. YAML and JSON already carry types; string values from .env are then interpreted as Python literals, so LIST=[1, 2] arrives as a list. When that guessing is unwanted, get_raw turns it off, and config.int(...) and friends cast explicitly, returning None rather than throwing when the value is not convertible.

Runtime mutation is a first-class case. set, insert (a dict or another file) and update_from_locals exist because configuration is often derived — composing a full name, joining a URL — and those derived values deserve to live in the same place as the ones read from disk.

Where it landed

Nine releases on PyPI, currently 1.3.6, and the most-starred library I have written. It is the project that taught me that an API’s real surface is its failure behaviour, not its happy path — almost every issue and change was about what happens when a value is not there.

sources it reads without being told
.yaml / .yml / .json
Parsed with native types — `port: 5050` arrives as an int.
.ini / .cfg
Standard section-based config files.
.py
Python config modules, skipped if they construct Config() themselves to avoid recursion.
.env / env_file
KEY=VALUE files, including values that parse as Python literals.
Environment variables
Existing and newly set, merged with the file sources.
dict
Plain dictionaries inserted at runtime via `config.insert(...)`.

Defaults are the names people already use, so most projects need no arguments at all.

accessors
config.name
Attribute access for the common case.
config['name']
Dict access — raises KeyError when absent.
config.get('a.b.c', default)
Dotted path to any depth, with a default instead of an exception.
config.int / float / str / list / dict
Typed getters that return None when the cast fails.
config.get_raw('key')
Opt out of type interpretation entirely.
config.assert_contains('key')
Fail loudly at startup if a required setting never loaded.

One value, several ways to ask for it — chosen by what you want to happen when it is missing.