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/cacheimport configparser
config = configparser.ConfigParser()
config.read("config.ini")
print(config["paths"]["cache"]) # /opt/myapp/data/cacheExtended 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}_cacheimport configparser
config = configparser.ConfigParser(
interpolation=configparser.ExtendedInterpolation()
)
config.read("config.ini")
print(config["database"]["connection_string"]) # MyApp_db@localhost
print(config["cache"]["prefix"]) # MyApp_cacheCustom 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.comMulti-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 defaultconfig = 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"]) # valueAllow No Value Options
For flags without values:
[features]
enable_cache
use_ssl
debug_mode = trueconfig = 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 Teamconfig = configparser.ConfigParser()
config.read("config.ini")
template = config["email"]["template"]
# Multiline string with leading whitespace preservedCustom 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: