Intermediate Python 📂 Modules · 6 of 6 37 min read

Python ConfigParser

Master Python's ConfigParser module for reading, writing, and managing INI configuration files. Learn every method (read, get, getint, getboolean, set, sections, has_section), the DEFAULT section inheritance model, basic and extended interpolation, and layered config loading. Includes a visual anatomy of an INI file, a DEFAULT-section fall-through diagram, five practical examples, and INI vs JSON vs YAML vs TOML comparison.

Section 01

The Story That Explains Config Files

The Restaurant Menu and the Kitchen
A restaurant has two things: recipes (how to cook) and a menu (what to charge, what to serve today). The recipes are fixed — they're the code. The menu changes: today's specials, weekend prices, whether the outdoor seating is open. You don't rewrite the recipe book every time you change a price. You just update the menu.

Your Python program is the same. The logic lives in code. The settings — database host, API key, log level, feature flags — live in a configuration file. Change the file, restart the app, done. No code edits, no redeploy, no risk of breaking the recipes.

ConfigParser is Python's built-in reader for the oldest and simplest config format there is: INI files. Sections in square brackets, key = value pairs underneath, comments starting with # or ;. It's been around since the 1980s Windows days, and it's still the go-to format when you want humans to edit config without breaking anything.

This tutorial covers Python's configparser module end-to-end: reading INI files, extracting typed values, using the special DEFAULT section, variable interpolation, writing files back out, and where INI beats JSON/YAML/TOML and where it doesn't.

💡
The Core Insight

Config files exist to separate what changes from what doesn't. Code is what your program does. Config is how it does it in this particular environment. Mix the two, and every environment change becomes a code change. Separate them, and one binary runs everywhere.


Section 02

Anatomy of an INI File

Before writing any Python, understand the file format. INI files have exactly four building blocks — you'll never need a fifth.

The Four Building Blocks of an INI File
app.ini # Main application settings [database] host = localhost port = 5432 user = admin ; use ; or # for comments [server] debug = true timeout = 30 SECTION HEADER Name in square brackets. Groups related settings. Access: config["database"] KEY (option name) Setting name. Case-insensitive by default. No quotes. VALUE Always read as STRING. Use getint / getboolean to convert types. # or ; COMMENT Everything after the marker on that line is ignored. Must be at the start of a line by default.

Every INI file is nothing more than these four blocks repeated. Purple headers group things. Blue keys name settings. Green values hold data. Grey comments explain.


Section 03

Reading a Config File — The Basics

Step 1 — Create a Sample File

# app.ini
[database]
host = localhost
port = 5432
user = admin
password = secret

[server]
debug = true
timeout = 30
workers = 4

[logging]
level = INFO
file = /var/log/app.log

Step 2 — Read It From Python

import configparser

# Create a parser and load the file
config = configparser.ConfigParser()
config.read("app.ini")

# Dict-style access — the modern way
print(config["database"]["host"])         # localhost
print(config["database"]["port"])         # 5432  (still a STRING!)
print(config["logging"]["level"])         # INFO

# Method-style access — same result
print(config.get("database", "host"))       # localhost
⚠️
Every Value Is a String — At First

config["database"]["port"] returns the string "5432", not the integer 5432. Doing port + 1 gives "54321" instead of 5433. Always use the typed accessors — getint, getboolean, getfloat — for anything that isn't literally a string.


Section 04

All ConfigParser Methods — Reference

MethodWhat It Does
read(filename)Load one or more INI files (accepts a list too)
read_file(fp)Load from an already-open file object
read_string(text)Load from a Python string
read_dict(d)Load from a nested dictionary
sections()List of section names (excludes DEFAULT)
options(section)List of keys in a section (includes DEFAULT keys)
items(section)All key-value pairs in the section
has_section(section)Returns True if section exists
has_option(section, key)Returns True if key exists in section
get(section, key)Get value as string
getint(section, key)Get value as int
getfloat(section, key)Get value as float
getboolean(section, key)Get value as bool (yes/no/true/false/1/0/on/off)
set(section, key, value)Set a value (must be a string)
add_section(section)Create a new section
remove_section(section)Delete a section entirely
remove_option(section, key)Delete a single key
write(fp)Write the config out to a file object

Iterating the Whole Config

import configparser

config = configparser.ConfigParser()
config.read("app.ini")

# Every section
for section in config.sections():
    print(f"[{section}]")
    for key, value in config.items(section):
        print(f"  {key} = {value}")
    print()

# Just the section names
print(config.sections())
# ['database', 'server', 'logging']

# Just the keys in one section
print(config.options("database"))
# ['host', 'port', 'user', 'password']

Checking Before Reading

import configparser

config = configparser.ConfigParser()
config.read("app.ini")

if config.has_section("cache"):
    ttl = config.getint("cache", "ttl")
else:
    ttl = 300  # fallback

if config.has_option("database", "replica_host"):
    replica = config["database"]["replica_host"]
else:
    replica = None

# Or use the fallback= parameter — cleaner
port = config.getint("database", "port", fallback=5432)
name = config.get("database", "name", fallback="myapp")

Section 05

Getting Values with Proper Types

The getint, getfloat, and getboolean accessors do more than convert — they raise clear errors when the value can't be converted, saving you from silent bugs.

import configparser

config = configparser.ConfigParser()
config.read("app.ini")

# Integer — raises ValueError if not a valid integer
port = config.getint("database", "port")
print(port + 1)                          # 5433  (now it's a real int)

# Float
timeout = config.getfloat("server", "timeout")
print(type(timeout))                      # <class 'float'>

# Boolean — accepts many spellings
debug = config.getboolean("server", "debug")
print(debug)                             # True

# All of these are accepted for booleans:
#   True  ← yes, true, on, 1
#   False ← no, false, off, 0

# With fallback — no crash if the key is missing
workers = config.getint("server", "workers", fallback=4)
🔑
Boolean Values Are Forgiving

getboolean understands true/false, yes/no, on/off, and 1/0 — case insensitive. Anything else raises ValueError. This flexibility is a feature: your users can write natural-looking config, and typos still fail loudly.


Section 06

The DEFAULT Section — Shared Values

The [DEFAULT] section is magic. Every key you put there becomes automatically visible in every other section — unless that section overrides it. Perfect for values that apply broadly but vary in specific environments.

How DEFAULT Values Fall Through to Other Sections
[DEFAULT] log_level = INFO timeout = 30 retries = 3 visible to ALL sections below [production] host = prod.db.co log_level = WARN ← overrides effective values: host = prod.db.co log_level = WARN timeout = 30 (from DEFAULT) retries = 3 (from DEFAULT) [staging] host = staging.db.co effective values: host = staging.db.co log_level = INFO (from DEFAULT) timeout = 30 (from DEFAULT) retries = 3 (from DEFAULT) [dev] host = localhost log_level = DEBUG effective values: host = localhost log_level = DEBUG timeout = 30 (from DEFAULT) retries = 3 (from DEFAULT)

Purple values flow down from [DEFAULT] into every section. A section can override a value by redefining it (red dashed line). Sections that don't mention a key inherit whatever DEFAULT has.

# app.ini
[DEFAULT]
log_level = INFO
timeout = 30
retries = 3

[production]
host = prod.db.co
log_level = WARN

[staging]
host = staging.db.co

[dev]
host = localhost
log_level = DEBUG
import configparser

config = configparser.ConfigParser()
config.read("app.ini")

# Notice: 'timeout' is NEVER defined in [staging] itself
# But it's readable because DEFAULT provides it
print(config["staging"]["host"])         # staging.db.co
print(config["staging"]["log_level"])    # INFO  (from DEFAULT)
print(config["staging"]["timeout"])      # 30   (from DEFAULT)

# [production] overrides log_level
print(config["production"]["log_level"]) # WARN (overridden)
print(config["production"]["timeout"])   # 30   (still from DEFAULT)

# sections() does NOT include DEFAULT — it's not a normal section
print(config.sections())
# ['production', 'staging', 'dev']

Section 07

Interpolation — Variable Substitution

ConfigParser supports interpolation: values can reference other values using %(key)s syntax (basic) or ${section:key} syntax (extended). Great for building paths from base directories, or URLs from hostnames.

Basic Interpolation — Same Section Only

# app.ini
[paths]
home = /home/alice
data = %(home)s/data
logs = %(home)s/logs
backup = %(data)s/backups
import configparser

config = configparser.ConfigParser()
config.read("app.ini")

print(config["paths"]["home"])       # /home/alice
print(config["paths"]["data"])       # /home/alice/data
print(config["paths"]["logs"])       # /home/alice/logs
print(config["paths"]["backup"])     # /home/alice/data/backups

Extended Interpolation — Cross-Section References

# app.ini
[server]
host = example.com
port = 8080

[api]
base_url = https://${server:host}:${server:port}/api
health_url = ${base_url}/health
users_url = ${base_url}/users
import configparser

# Extended interpolation supports ${section:key} references
config = configparser.ConfigParser(
    interpolation=configparser.ExtendedInterpolation()
)
config.read("app.ini")

print(config["api"]["base_url"])
# https://example.com:8080/api

print(config["api"]["users_url"])
# https://example.com:8080/api/users
⚠️
Escape Literal Percent Signs

With basic interpolation on, % is a special character. To write a literal percent — say in a password or format string — use %%. Or disable interpolation entirely: ConfigParser(interpolation=None).


Section 08

Writing & Updating Config Files

Modify Values in Memory, Then Save

import configparser

config = configparser.ConfigParser()
config.read("app.ini")

# Update an existing value — must be a STRING
config["database"]["port"] = "5433"

# Add a whole new section
config["cache"] = {
    "backend": "redis",
    "host": "cache.example.com",
    "ttl": "3600"
}

# Or add a new key to an existing section
config["server"]["host"] = "0.0.0.0"

# Write it back to disk
with open("app.ini", "w") as f:
    config.write(f)

Removing Sections and Options

import configparser

config = configparser.ConfigParser()
config.read("app.ini")

# Delete a single key
config.remove_option("database", "password")

# Delete a whole section (and all its keys)
config.remove_section("logging")

# Both return True if removed, False if it wasn't there

# Persist the changes
with open("app.ini", "w") as f:
    config.write(f)

Building a Config File From Scratch

import configparser

config = configparser.ConfigParser()

# Dict-of-dicts is the cleanest way to build
config["DEFAULT"] = {"log_level": "INFO", "timeout": "30"}
config["database"] = {
    "host": "localhost",
    "port": "5432",
    "user": "admin"
}
config["server"] = {
    "debug": "false",
    "workers": "4"
}

with open("new_config.ini", "w") as f:
    config.write(f)

Section 09

Reading From Multiple Files

A powerful pattern: layer configs. Load a base config, then load an environment- specific one that overrides only what needs changing. Load a user config last for final overrides.

import configparser

config = configparser.ConfigParser()

# Files loaded LATER override values from earlier ones
config.read([
    "defaults.ini",       # shipped with the app
    "/etc/app.ini",       # system-wide config
    "~/.app.ini",          # user's home directory
    "./app.ini"            # current directory (highest priority)
])

# Missing files are silently skipped — read() returns the list of files
# that were actually loaded
loaded = config.read(["a.ini", "b.ini"])
print(f"Loaded: {loaded}")
📈
The Layered Config Pattern

Ship sensible defaults with your code. Let sysadmins tune per-machine settings via /etc/. Let users personalise via their home directory. Each layer overrides only what it cares about — the rest falls through. This is how professional applications handle configuration.


Section 10

Practical Real-World Examples

Example 1 — Database Connection From Config

import configparser

def get_db_config(path="app.ini"):
    config = configparser.ConfigParser()
    config.read(path)

    return {
        "host": config.get("database", "host", fallback="localhost"),
        "port": config.getint("database", "port", fallback=5432),
        "user": config.get("database", "user"),
        "password": config.get("database", "password"),
        "dbname": config.get("database", "dbname", fallback="myapp"),
    }

db = get_db_config()
print(f"Connecting to {db['host']}:{db['port']}")

Example 2 — Environment Switcher

import configparser
import os

env = os.getenv("APP_ENV", "dev")

config = configparser.ConfigParser()
config.read("config.ini")

# config.ini has [dev], [staging], [production] sections
# with a [DEFAULT] section for shared values
if not config.has_section(env):
    raise ValueError(f"Unknown environment: {env}")

active = config[env]
print(f"Running in {env} mode")
print(f"Database: {active['db_host']}")
print(f"Debug:    {config.getboolean(env, 'debug')}")

Example 3 — Feature Flags

import configparser

# flags.ini
# [features]
# dark_mode = true
# new_dashboard = false
# beta_analytics = on

config = configparser.ConfigParser()
config.read("flags.ini")

def flag_enabled(name):
    return config.getboolean("features", name, fallback=False)

if flag_enabled("dark_mode"):
    print("🌙  Using dark theme")

if flag_enabled("new_dashboard"):
    print("Loading new dashboard")
else:
    print("Loading classic dashboard")

Example 4 — API Client Setup

import configparser

config = configparser.ConfigParser(
    interpolation=configparser.ExtendedInterpolation()
)
config.read_string("""
[server]
protocol = https
host = api.example.com
port = 443

[endpoints]
base = ${server:protocol}://${server:host}:${server:port}
users = ${base}/v1/users
orders = ${base}/v1/orders
""")

print(config["endpoints"]["users"])
# https://api.example.com:443/v1/users

print(config["endpoints"]["orders"])
# https://api.example.com:443/v1/orders

Example 5 — Save User Preferences

import configparser
from pathlib import Path

CONFIG_PATH = Path.home() / ".myapp.ini"

def load_prefs():
    config = configparser.ConfigParser()
    if CONFIG_PATH.exists():
        config.read(CONFIG_PATH)
    return config

def save_prefs(config):
    with open(CONFIG_PATH, "w") as f:
        config.write(f)

# Load, modify, save
prefs = load_prefs()
if not prefs.has_section("ui"):
    prefs.add_section("ui")

prefs["ui"]["theme"] = "dark"
prefs["ui"]["font_size"] = "14"
prefs["ui"]["language"] = "en"

save_prefs(prefs)
print(f"Preferences saved to {CONFIG_PATH}")

Section 11

INI vs JSON vs YAML vs TOML

ConfigParser handles INI files, but Python has options. Here's when to pick which.

FormatBest ForWeakness
INI Simple flat config, human-editable, no dependencies No nested structures, no arrays, no types
JSON Machine-generated config, API responses, data exchange No comments allowed — painful for humans to maintain
YAML Rich nested structures, cross-language (Kubernetes, CI) Whitespace-sensitive, complex spec, needs PyYAML package
TOML Modern Python projects (pyproject.toml) Nesting syntax gets awkward, less familiar to non-developers
🔑
When INI Is The Right Choice

Pick INI when the config is flat (2 levels max), humans will edit it, and you don't want any dependencies. Pick TOML for anything Python-tool-related (it's the standard for pyproject.toml). Pick YAML for deeply nested or DevOps-facing config. Pick JSON only when generated by machines — the no-comments rule makes it painful for humans.


Section 12

Common Pitfalls

⚠️
Everything Is a String
Even numbers and booleans
config["s"]["port"] returns "5432", not 5432. Use getint, getboolean, getfloat. When calling set(), pass strings only.
🚫
Case-Insensitive Keys
Host = HOST = host
By default, ConfigParser lowercases all keys. Host and HOST collapse to host. If you need case preservation, subclass or set optionxform = str.
🔑
Percent Signs Are Special
%(x)s is interpolation
Passwords with %, format strings, URLs with URL-encoded chars — all break basic interpolation. Escape as %%, or disable: ConfigParser(interpolation=None).
🔥
Silent Missing Files
read() doesn't raise
config.read("missing.ini") returns silently — no error, empty config. Check the return value (list of loaded files) or use read_file() with an open() that will raise.
📌
DEFAULT Isn't a Section
Not in sections()
config.sections() excludes [DEFAULT]. It's accessed via config.defaults() or by reading through any other section (values fall through).
🛡
No Nested Structure
Two levels max
INI can't represent lists or nested dicts. If you need servers[] or database.credentials.password, use TOML or YAML instead — forcing it into INI leads to string parsing hacks.

Section 13

Golden Rules

🔑 ConfigParser — Non-Negotiable Rules
1
Always use typed accessors for non-string values. getint, getboolean, getfloat. Never int(config["s"]["port"]) in application code — typed accessors give better error messages and support fallbacks.
2
Provide fallbacks for optional values. config.get("s", "key", fallback="default"). Missing keys should be a design choice, not a crash. Fallbacks make configs backward-compatible when you add new options in later releases.
3
Use [DEFAULT] for values shared across sections. Timeout, log level, retry count — things that vary rarely per environment. Override in specific sections only when they differ. Halves your file size and prevents drift.
4
Never store secrets in checked-in config files. Passwords, API keys, tokens: use environment variables, a secrets manager, or a separate uncommitted secrets.ini. Git history is forever — a leaked credential in a committed file lives on even after removal.
5
Load configs in priority order. config.read([defaults, system, user, local]). Later files override earlier ones. This lets defaults ship with the app while allowing per-user and per-environment overrides without duplicating config.
6
Check read()'s return value in critical paths. Silent failure to load a required config file causes mystery bugs. If a config is required, use read_file(open(path)) — it raises FileNotFoundError loudly.
7
Match the format to the shape of your data. Flat, human-edited config → INI. Nested, DevOps-facing → YAML. Modern Python tooling → TOML. Machine-generated → JSON. Force-fitting INI into a nested structure creates worse code than picking the right tool.
You have completed Modules. View all sections →