The configparser module handles INI-style configs. Here's how to unlock its full power.

Variable Interpolation

Reference other values within your config:

# config.ini
[paths]
base = /opt/myapp
data = %(base)s/data
logs = %(base)s/logs
cache = %(data)s/cache
import configparser
 
config = configparser.ConfigParser()
config.read("config.ini")
 
print(config["paths"]["cache"])  # /opt/myapp/data/cache

Extended Interpolation

Access values from other sections:

# config.ini
[DEFAULT]
app_name = MyApp
 
[database]
host = localhost
connection_string = ${app_name}_db@${database:host}
 
[cache]
prefix = ${DEFAULT:app_name}_cache
import configparser
 
config = configparser.ConfigParser(
    interpolation=configparser.ExtendedInterpolation()
)
config.read("config.ini")
 
print(config["database"]["connection_string"])  # MyApp_db@localhost
print(config["cache"]["prefix"])  # MyApp_cache

Custom Type Converters

Beyond the built-in getint, getfloat, getboolean:

import configparser
import json
from pathlib import Path
 
def parse_list(value):
    return [v.strip() for v in value.split(",")]
 
def parse_path(value):
    return Path(value).expanduser()
 
def parse_json(value):
    return json.loads(value)
 
config = configparser.ConfigParser(
    converters={
        "list": parse_list,
        "path": parse_path,
        "json": parse_json,
    }
)
 
config.read_string("""
[settings]
hosts = localhost, db.example.com, cache.local
data_dir = ~/myapp/data
metadata = {"version": 1, "active": true}
""")
 
hosts = config.getlist("settings", "hosts")
# ['localhost', 'db.example.com', 'cache.local']
 
data_dir = config.getpath("settings", "data_dir")
# PosixPath('/home/user/myapp/data')
 
metadata = config.getjson("settings", "metadata")
# {'version': 1, 'active': True}

Environment Variable Interpolation

import configparser
import os
 
class EnvInterpolation(configparser.BasicInterpolation):
    def before_get(self, parser, section, option, value, defaults):
        value = super().before_get(parser, section, option, value, defaults)
        # Replace ${ENV_VAR} with environment variable
        import re
        pattern = r'\$\{([^}]+)\}'
        def replace(match):
            return os.environ.get(match.group(1), match.group(0))
        return re.sub(pattern, replace, value)
 
config = configparser.ConfigParser(interpolation=EnvInterpolation())
config.read_string("""
[database]
host = ${DB_HOST}
password = ${DB_PASSWORD}
""")
 
os.environ["DB_HOST"] = "prod-db.example.com"
os.environ["DB_PASSWORD"] = "secret123"
 
print(config["database"]["host"])  # prod-db.example.com

Multi-File Configuration

Layer configs with overrides:

import configparser
from pathlib import Path
 
config = configparser.ConfigParser()
 
# Read in order: later files override earlier
config.read([
    "/etc/myapp/defaults.ini",
    "/etc/myapp/config.ini",
    Path.home() / ".myapp.ini",
    "local.ini",
])

Fallback Values

import configparser
 
config = configparser.ConfigParser()
config.read("config.ini")
 
# Default if missing
timeout = config.get("server", "timeout", fallback="30")
debug = config.getboolean("server", "debug", fallback=False)
 
# Check existence
if config.has_option("database", "pool_size"):
    pool = config.getint("database", "pool_size")

DEFAULT Section

Values inherited by all sections:

[DEFAULT]
timeout = 30
retry = 3
 
[api]
host = api.example.com
# timeout and retry inherited
 
[database]
host = db.example.com
timeout = 60  # Override default
config = configparser.ConfigParser()
config.read("config.ini")
 
print(config["api"]["timeout"])      # 30 (from DEFAULT)
print(config["database"]["timeout"]) # 60 (overridden)

Case Sensitivity

# Default: option names are lowercased
config = configparser.ConfigParser()
config.read_string("[section]\nMyKey = value")
print(config["section"]["mykey"])  # value
 
# Preserve case
config = configparser.ConfigParser()
config.optionxform = str  # Don't transform option names
config.read_string("[section]\nMyKey = value")
print(config["section"]["MyKey"])  # value

Allow No Value Options

For flags without values:

[features]
enable_cache
use_ssl
debug_mode = true
config = configparser.ConfigParser(allow_no_value=True)
config.read("config.ini")
 
# Options without values are None
if config.has_option("features", "enable_cache"):
    print("Cache enabled")

Writing Configuration

import configparser
 
config = configparser.ConfigParser()
config["server"] = {
    "host": "localhost",
    "port": "8080",
    "workers": "4"
}
config["database"] = {
    "url": "postgres://localhost/mydb"
}
 
# Write to file
with open("config.ini", "w") as f:
    config.write(f)

Validation Pattern

import configparser
from dataclasses import dataclass
from pathlib import Path
 
@dataclass
class Config:
    db_host: str
    db_port: int
    log_path: Path
    debug: bool
 
def load_config(path: str) -> Config:
    parser = configparser.ConfigParser()
    parser.read(path)
    
    return Config(
        db_host=parser.get("database", "host"),
        db_port=parser.getint("database", "port"),
        log_path=Path(parser.get("logging", "path")),
        debug=parser.getboolean("app", "debug", fallback=False),
    )
 
config = load_config("app.ini")

Multiline Values

[email]
template = Welcome to our service!
    
    Your account is ready.
    
    Best regards,
    The Team
config = configparser.ConfigParser()
config.read("config.ini")
 
template = config["email"]["template"]
# Multiline string with leading whitespace preserved

Custom Delimiters

# Use : instead of = for assignment
config = configparser.ConfigParser(delimiters=(":",))
 
# Use # only for comments (not ;)
config = configparser.ConfigParser(comment_prefixes=("#",))

Strict Mode

# Disallow duplicate options/sections
config = configparser.ConfigParser(strict=True)
 
try:
    config.read_string("""
    [section]
    key = value1
    key = value2
    """)
except configparser.DuplicateOptionError as e:
    print(f"Duplicate: {e}")

Real-World: Application Config System

import configparser
import os
from pathlib import Path
from typing import Optional
 
class AppConfig:
    def __init__(self, app_name: str):
        self.app_name = app_name
        self._parser = configparser.ConfigParser(
            interpolation=configparser.ExtendedInterpolation(),
            converters={"list": lambda v: [x.strip() for x in v.split(",")]}
        )
        self._load()
    
    def _load(self):
        config_files = [
            Path(__file__).parent / "defaults.ini",
            Path(f"/etc/{self.app_name}/config.ini"),
            Path.home() / f".{self.app_name}.ini",
            Path("config.ini"),
        ]
        self._parser.read(config_files)
        
        # Override with environment variables
        for section in self._parser.sections():
            for key in self._parser[section]:
                env_key = f"{self.app_name.upper()}_{section.upper()}_{key.upper()}"
                if env_key in os.environ:
                    self._parser[section][key] = os.environ[env_key]
    
    def get(self, section: str, key: str, 
            fallback: Optional[str] = None) -> str:
        return self._parser.get(section, key, fallback=fallback)
    
    def getint(self, section: str, key: str, 
               fallback: int = 0) -> int:
        return self._parser.getint(section, key, fallback=fallback)
    
    def getlist(self, section: str, key: str) -> list:
        return self._parser.getlist(section, key)
 
# Usage
config = AppConfig("myapp")
db_host = config.get("database", "host", fallback="localhost")

configparser scales from simple key-value files to sophisticated layered configurations. It's the right choice when you want human-editable config without the complexity of YAML or TOML.

React to this post: