configparser: Read and Write INI Files

Updated on: August 20, 2026
Reading time: 5 minutes
File and system diagram representing INI configuration with Python configparser

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.log

Each 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 = 5

Default 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 = str

Set 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/cache

Write %% 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}/logs

Use $$ 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
audit

These 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 line

For 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 exc

Include 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, port

The 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.

  • 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.

Share:

Facebook
WhatsApp
Twitter
LinkedIn

Article content

    Related articles

    Developer configuring a Python HTTPS server and TLS certificate
    Advanced Python
    Foto de perfil de Leandro Hirt da Academify

    HTTPSServer: Build a Local HTTPS Server in Python

    Learn Python HTTPSServer for local HTTPS services, TLS certificates, threaded handling, testing, and practical security limits.

    Ler mais

    Tempo de leitura: 4 minutos
    08/10/2026
    Protected files representing secure TAR extraction with Python
    Advanced Python
    Foto de perfil de Leandro Hirt da Academify

    tarfile extraction_filter: Extract TAR Safely

    Learn Python tarfile extraction_filter for safer TAR extraction, path validation, links, permissions, and resource limits.

    Ler mais

    Tempo de leitura: 5 minutos
    08/10/2026
    Python code showing warnings controlled with catch_warnings
    Advanced Python
    Foto de perfil de Leandro Hirt da Academify

    catch_warnings: Capture Python Warnings in Tests

    Learn Python catch_warnings to capture, test, and control warnings with focused filters and safe temporary scopes.

    Ler mais

    Tempo de leitura: 5 minutos
    07/10/2026
    Python code representing persistent pickle references
    Advanced Python
    Foto de perfil de Leandro Hirt da Academify

    pickle persistent_id: Serialize External References

    Learn Python pickle persistent_id for stable external references, validation, security, performance, and long-term compatibility.

    Ler mais

    Tempo de leitura: 5 minutos
    07/10/2026
    Binary code representing Python buffers and memory views
    Advanced Python
    Foto de perfil de Leandro Hirt da Academify

    memoryview.count: Count Values Without Buffer Copies

    Learn Python memoryview.count to count bytes and values in buffers without copies, with formats, limits, and practical safety.

    Ler mais

    Tempo de leitura: 5 minutos
    06/10/2026
    Python code and type annotations
    Advanced Python
    Foto de perfil de Leandro Hirt da Academify

    annotationlib: Avoid Circular Imports in Annotations

    Learn Python annotationlib to inspect deferred annotations, avoid circular imports, and build safer runtime tooling.

    Ler mais

    Tempo de leitura: 5 minutos
    06/10/2026