Python configparser reads and writes INI-style configuration files organized into sections, options, and string values. It is a practical fit for desktop programs, command-line tools, administrative scripts, and applications that need human-editable settings without requiring JSON, YAML, or a database.
INI is not governed by one universal specification. Applications differ in their rules for comments, capitalization, delimiters, empty options, multiline values, and interpolation. Configure the parser deliberately and document the exact dialect your users may write.
Create a basic INI file
[server]
host = 127.0.0.1
port = 8080
debug = false
[logging]
level = INFO
file = logs/app.logEach bracketed header defines a section. Options use = or : as delimiters by default.
Read configuration safely
import configparser
config = configparser.ConfigParser()
loaded = config.read("app.ini", encoding="utf-8")
if not loaded:
raise FileNotFoundError("app.ini was not loaded")
host = config["server"]["host"]
port = config["server"].getint("port")
debug = config["server"].getboolean("debug")read() silently ignores files that cannot be opened and returns the successfully parsed filenames. Check the return value for required files or use read_file() with an already opened handle.
All values are stored as strings
The parser does not infer data types. Use getint(), getfloat(), and getboolean().
timeout = config["server"].getfloat("timeout", fallback=5.0)
workers = config["server"].getint("workers", fallback=4)
enabled = config["server"].getboolean("enabled", fallback=True)getboolean() recognizes values such as yes/no, true/false, on/off, and 1/0. Do not call bool("false"), because every nonempty string is true.
Use fallback values correctly
On a section proxy, the second argument to get() is the fallback:
level = config["logging"].get("level", "INFO")On the parser-level API, use the keyword-only argument:
level = config.get("logging", "level", fallback="INFO")Values inherited from DEFAULT take precedence over a fallback, which can surprise code that checks only the selected section.
Define shared defaults
[DEFAULT]
timeout = 10
retries = 3
[api]
url = https://api.example.com
[worker]
retries = 5Default options are visible through every section. Removing a section-specific override reveals the default again because it was never physically stored in that section.
Layer several files
Files read later override conflicting options while preserving everything else.
config.read(
["defaults.ini", "site.ini", "user.ini"],
encoding="utf-8",
)A common pattern uses versioned defaults, host-level settings, and user overrides. Validate mandatory values after all layers are loaded.
Use read_file for mandatory input
from pathlib import Path
path = Path("defaults.ini")
with path.open(encoding="utf-8") as handle:
config.read_file(handle, source=str(path))Open and parsing errors propagate, and the source label improves diagnostics.
Read strings and dictionaries
read_string() is convenient for tests and controlled embedded configuration.
config.read_string("""
[service]
url = https://example.com
""", source="embedded defaults")read_dict() accepts nested mappings and converts keys and values to strings.
config.read_dict({
"server": {"port": 8080, "debug": False},
})Understand option-name case
Section names are case-sensitive by default, but option names are not. optionxform() normally converts keys to lowercase.
config = configparser.ConfigParser()
config.read_string("""
[Section]
MyKey = value
""")
print(list(config["Section"]))
# ['mykey']To preserve original capitalization:
config.optionxform = strSet it before reading data. A custom canonicalization function should be idempotent.
Basic interpolation
ConfigParser enables BasicInterpolation by default.
[paths]
base = /opt/myapp
logs = %(base)s/logs
cache = %(base)s/cacheWrite %% for a literal percent sign. Interpolation occurs when a value is retrieved, not during parsing.
Extended interpolation
ExtendedInterpolation supports ${section:option} and references between sections.
from configparser import ConfigParser, ExtendedInterpolation
config = ConfigParser(interpolation=ExtendedInterpolation())[common]
root = /srv/app
[logging]
dir = ${common:root}/logsUse $$ for a literal dollar sign. Missing options and reference loops raise interpolation exceptions.
Disable interpolation when appropriate
Passwords, templates, regular expressions, and shell fragments may legitimately contain % or $. If substitution is not part of the format:
config = configparser.ConfigParser(interpolation=None)For one lookup, use raw=True.
Add custom converters
The converters argument creates new get* methods.
from pathlib import Path
config = configparser.ConfigParser(
converters={
"path": Path,
"list": lambda value: [item.strip() for item in value.split(",")],
},
)
log_path = config["logging"].getpath("file")
origins = config["cors"].getlist("origins")Conversion is not complete validation. Check ranges, extensions, filesystem roots, existence, and business constraints afterward.
Customize Boolean values
config.BOOLEAN_STATES = {
"enabled": True,
"disabled": False,
}A custom vocabulary may improve readability but reduces portability. Document accepted spellings.
Allow options without values
Some existing INI dialects use bare flags.
config = configparser.ConfigParser(allow_no_value=True)[features]
safe_mode
auditThese options produce None. Recent Python versions raise MultilineContinuationError if a valueless option is followed by an indented continuation line.
Unnamed sections
Python 3.13 added optional support for initial options before the first section header.
config = configparser.ConfigParser(allow_unnamed_section=True)
config.read_string("""
key = value
[other]
x = 1
""")
value = config[configparser.UNNAMED_SECTION]["key"]Enable this only for compatibility with a format that already requires it.
Comments and multiline values
By default, # and ; introduce comments on otherwise empty lines. Inline comments are disabled because comment-prefix characters cannot be escaped reliably.
Multiline values must be indented deeper than their key:
[message]
text = first line
second line
third lineFor human-maintained files, consider empty_lines_in_values=False to reduce visual ambiguity.
Keep strict mode enabled
strict=True, the modern default, rejects duplicate sections and options within one source. This catches spelling mistakes and case-normalization collisions. Keep it enabled for new applications.
Handle parsing errors clearly
try:
with open("app.ini", encoding="utf-8") as handle:
config.read_file(handle)
except configparser.MissingSectionHeaderError as exc:
raise RuntimeError("Missing section header") from exc
except configparser.DuplicateOptionError as exc:
raise RuntimeError("Duplicate option") from exc
except configparser.InterpolationError as exc:
raise RuntimeError("Invalid interpolation") from excInclude source and line information in diagnostics, but never print secret values.
Write configuration
from pathlib import Path
config["server"]["port"] = "9090"
with Path("app.ini").open("w", encoding="utf-8") as handle:
config.write(handle)All values must be strings. In Python 3.14, write() raises InvalidWriteError when the generated representation could not be read back accurately.
Write atomically
Do not overwrite an important file directly. Write to a temporary file in the same directory, flush and synchronize it, then replace the destination.
import os
from pathlib import Path
from tempfile import NamedTemporaryFile
destination = Path("app.ini")
with NamedTemporaryFile("w", encoding="utf-8", dir=destination.parent, delete=False) as temp:
config.write(temp)
temp.flush()
os.fsync(temp.fileno())
temporary = temp.name
os.replace(temporary, destination)Control file permissions and use locking or a central service when several writers may update configuration.
Comments are not preserved
Reading and writing a file again loses original comments and formatting. If users maintain important explanations in the file, avoid automatic rewrites or choose a parser that preserves layout.
Do not store secrets in INI
INI files are plain text. Passwords, tokens, and private keys should come from a secret manager, protected environment variable, or operating-system credential store. Store only a secret identifier when configuration needs a reference.
The Python site guide explains installation paths and environments, Python importlib.resources can distribute packaged defaults, and Python fileinput handles multi-file processing.
Validate after parsing
def load_server(config):
port = config["server"].getint("port")
if not 1 <= port <= 65535:
raise ValueError("port is outside the valid range")
host = config["server"].get("host", "127.0.0.1").strip()
if not host:
raise ValueError("host is empty")
return host, portThe parser validates structure and basic conversions; the application must enforce semantics.
ConfigParser, TOML, and JSON
INI is approachable for small settings. TOML has a stronger specification and native data types. JSON is widely interoperable but does not support comments. Choose based on complexity, ecosystem, and human-editing requirements.
Recommended practices
- Open files explicitly as UTF-8.
- Keep
strict=True. - Verify required files.
- Define case-normalization behavior.
- Disable interpolation when unused.
- Validate every converted value.
- Write updates atomically.
- Keep secrets outside the INI file.
Conclusion
Python configparser provides a mature interface for INI configuration with sections, defaults, layered files, interpolation, converters, and mapping-style access. It works best when the accepted dialect remains simple and documented.
Consult the official configparser documentation and the Python tomllib documentation.







