Advertisement
Open Source Projects by Phil Schwartz

Building Hierarchical Settings in Python With configparser

Most Python developers eventually hit configuration sprawl. One file for the database, another for feature flags, a third for environment overrides, and nobody remembers which file wins. A clean approach using the standard library can save a Sydney or Melbourne-based team from those headaches without pulling in YAML parsers or TOML libraries.

Python ships with configparser, a module that has lived in the standard library for years. It reads and writes INI-style files, supports sections, and tolerates comments. It does not, however, understand nested sections out of the box. Developers who want tree-shaped settings usually reach for external packages, but a small amount of careful design lets configparser express enough hierarchy for most needs.

The trick is to treat configparser as a storage format rather than a finished API. By adding thin wrapper code and a few naming conventions, you can represent overrides, defaults, and environment-specific values without leaving the standard library. The result travels well inside open-source projects and survives the move from a developer's laptop in Adelaide to a production cluster in ap-southeast-2.

For projects like DenyHosts or similar utilities published from Australia to a global audience, this approach fits the rest of a typical Python toolchain. You can version the .ini files alongside the code, diff them in pull requests, and keep secrets out of source control by layering files. Simplicity matters more than any syntax sugar a heavier format offers.

Why configparser still earns its place

Many newcomers assume configparser is a relic. The reality is more nuanced. The module handles interpolation, type conversion through getters, and case sensitivity options that cover most application scenarios. Compared to pulling in PyYAML or a TOML library, configparser adds zero install weight and zero supply-chain risk.

For teams shipping open-source tools, that distinction matters. A Brisbane maintainer who distributes a security utility cannot assume every downstream user has the latest version of an optional dependency. Sticking with the standard library means the loader keeps working on a stripped-down Alpine container or an LTS distribution pinned to an older Python.

There is also a documentation angle. New contributors from local meetups, whether PyCon AU or a smaller gathering in Perth, can open an .ini file in any editor and immediately understand the structure, without learning a new syntax or remembering whether the format uses tabs or spaces.

Starting with a flat configuration file

Before adding hierarchy, it helps to see what configparser offers directly. A basic settings file might hold a [database] section for connection details, a [logging] section for verbosity, and a [features] section for toggles. Each section stores key-value pairs, and configparser exposes them through a dictionary-like interface.

Reading such a file takes three lines: instantiate ConfigParser(), call read(), then look up values with get() or getint(). Interpolation, using the ${section:key} syntax, lets one setting reference another without manual string formatting. The catch appears when the module is asked to represent an environment that inherits from another, since the standard library does not provide nested sections.

Using DEFAULT for shared values

Every section in a configparser file inherits from a magic section called DEFAULT. Any key placed there is visible inside every other section unless that section overrides it. That single behaviour is enough to express a clean layer of shared settings.

Consider an application that runs in Sydney for development and in Singapore for staging. Both environments share a database driver and a logging level, but differ in host and port. Placing shared keys under [DEFAULT] and environment-specific keys inside their own sections keeps the file short and lets a feature flag default to off without forcing every section to repeat the value.

Simulating hierarchy with dotted keys

Some configuration languages treat dots as path separators, so database.pool.size reads as a three-level path. Configparser cannot do that natively, but the same idea can be encoded with a naming convention. Keys like database__pool__size or database/pool/size are flat strings to the parser, yet they carry hierarchical meaning to the application.

A small helper function can split the key on the chosen separator and walk the resulting path inside a nested dictionary. That helper turns the flat INI file into a tree at runtime while keeping the on-disk format plain and editable. The separator choice is mostly taste: double underscores survive shells without quoting, while forward slashes read like paths and feel natural to developers who have written Apache-style configs.

Wrapping the parser in a settings class

Once a team settles on a hierarchy style, wrapping configparser inside a dedicated class pays off. The class can expose nested access through attribute or item syntax, validate types on read, and hide interpolation details from the rest of the codebase. It also becomes the single place to add environment-aware loading later.

A common pattern is a Settings class that accepts a list of file paths in priority order, merges them in turn, and returns a deeply nested view of the final result. The class can cache reads, support hot-reload during development, and raise clear errors when a required key is missing. For a DenyHosts-style utility, this layer also makes it easier to build a Python interactive shell, where settings become explorable objects rather than opaque strings.

Loading layered files by environment

Hierarchical settings earn their keep when an application runs in multiple environments. A common layout is three files: a defaults.ini checked into source control, an environment.ini describing the current deployment, and a local.ini kept outside version control for personal tweaks. Each layer overrides the keys above it.

Configparser can read multiple files in sequence, with later calls to read() overwriting earlier values. The wrapper class orchestrates this ordering based on an environment variable or a command-line flag. A Melbourne-based staging cluster and a Brisbane-based production cluster can share the same defaults while diverging on endpoints and credentials, and developers running the tool from Adelaide or Perth can drop their own local.ini next to the binary for personal overrides.

Validating values and handling missing keys

Reading configuration is only half the job. The other half is catching errors early, before they reach a database call or a network request. Wrapping every get() call in a validator that checks types and ranges turns vague KeyError tracebacks into messages a human can act on.

A reasonable default is to fail loudly when a required key is absent and to fall back silently when an optional key is missing. That asymmetry matches how most teams use configuration: mandatory values crash at startup, optional ones let the code take a sensible path. When the tree grows beyond a handful of keys, generating a small dataclass from the parsed values becomes attractive, and a hand-written Settings class with explicit type hints keeps the project close to the standard library.