Python ConfigParser Tutorial: Read and Write Configuration Files
Learn how to read, validate, update, and write INI-style configuration files with Python’s standard-library configparser module.
Python’s standard-library configparser module reads and writes INI-style configuration files. Use read_file() when a file is required, read() when configuration files are optional, typed getters when values need conversion, and write() to save changes.
For example, this file defines shared defaults and application-specific settings:
[DEFAULT]
log_level = INFO
timeout = 30
[service]
base_url = https://api.example.com
retries = 3
enabled = yes
The examples below use only the Python standard library. See the official configparser reference for version-specific details.
1. Create a parser and read a required file
ConfigParser reads a configuration language with sections and key/value options. For a file your application must have, open it explicitly and pass the text file object to read_file(). This makes a missing file or an invalid configuration an explicit error instead of silently continuing with an empty parser.
import configparser
from pathlib import Path
config_path = Path("settings.ini")
config = configparser.ConfigParser()
with config_path.open(encoding="utf-8") as config_file:
config.read_file(config_file)
print(config["service"]["base_url"])
print(config.getint("service", "retries"))
print(config.getboolean("service", "enabled"))
Save the sample INI from the introduction as settings.ini to run this example. The context manager closes the file after parsing. Use a deliberate encoding, commonly UTF-8, so configuration text is read consistently.
2. Read optional files and layer overrides
read() is useful when configuration files are optional. It ignores paths it cannot open and returns the names it successfully parsed. If none exist, the parser can remain empty, so check the returned list when you need to know whether any file was found.
import configparser
config = configparser.ConfigParser()
loaded = config.read(
["defaults.ini", "settings.ini", "local.ini"],
encoding="utf-8",
)
print("Loaded:", loaded)
if not loaded:
raise FileNotFoundError("No configuration file was found")
Files are applied in order. Later files override earlier values when they define the same section and option; options found only in earlier files remain available. This lets a project ship defaults and let a local file override selected settings. For mandatory input, prefer read_file() and let opening or parsing failures surface.
3. Retrieve values and convert them to the needed type
Values are strings at the parser boundary. You can use mapping syntax or get() for strings, and the built-in typed getters for integers, floating-point numbers, and booleans.
base_url = config["service"]["base_url"]
# Equivalent string lookup:
base_url = config.get("service", "base_url")
retries = config.getint("service", "retries")
ratio = config.getfloat("service", "retry_ratio", fallback=1.5)
enabled = config.getboolean("service", "enabled")
print(base_url, retries, ratio, enabled)
getboolean() recognizes common true values such as yes, true, on, and 1, and corresponding false values such as no, false, off, and 0, case-insensitively. Other values raise a conversion error. Integer and float getters likewise fail if the text cannot be converted.
A missing section or option normally raises an error when accessed directly. Use fallback= when absence is a legitimate case:
timeout = config.getint("service", "timeout", fallback=10)
optional_label = config.get("service", "label", fallback="default")
For application-specific types, register a converter when creating the parser. A converter named duration creates a getduration() method:
import configparser
config = configparser.ConfigParser(
converters={"duration": lambda value: float(value.rstrip("s"))}
)
config.read_string("[service]\ninterval = 2.5s\n")
interval_seconds = config.getduration("service", "interval")
print(interval_seconds)
This example is intentionally small; validate converter input and raise a clear error for the formats your application does not accept.
4. Understand DEFAULT values and interpolation
Options in [DEFAULT] act as inherited defaults: they can be retrieved from other sections unless that section supplies its own value. They are not ordinary named sections, and they do not behave like an independent application section.
[DEFAULT]
timeout = 30
[service]
base_url = https://api.example.com
timeout = config.getint("service", "timeout") # 30
Basic interpolation is enabled by default. It substitutes %(option)s references using values from the same section or defaults. A literal percent sign in an interpolated value must be escaped as %%.
[paths]
root = /srv/app
logs = %(root)s/logs
logs = config.get("paths", "logs") # /srv/app/logs
Choose the interpolation mode to match the file you consume:
- Basic interpolation: the default; uses
%(name)s. - Extended interpolation: supports references such as
${section:option}and${option}. - No interpolation: pass
interpolation=Noneto keep percent and dollar sequences literal. - One raw lookup: pass
raw=Truetoget()to skip interpolation for that call.
extended = configparser.ConfigParser(
interpolation=configparser.ExtendedInterpolation()
)
literal_values = configparser.ConfigParser(interpolation=None)
raw_template = config.get("service", "template", raw=True)
Interpolation is useful for avoiding repeated values, but malformed or missing references can raise interpolation errors when read. Disable it if the file is expected to contain literal percent-based formats such as templates or percent-encoded data.
5. Add or update values, then write the file
Assign strings to a section mapping to add or replace options. A section must exist before you assign through config[section]; use add_section() for a new one. Serialize with write() to a text-mode file object.
import configparser
config = configparser.ConfigParser()
config.read("settings.ini", encoding="utf-8")
if not config.has_section("service"):
config.add_section("service")
config["service"]["retries"] = "5"
config["service"]["enabled"] = "true"
with open("settings.ini", "w", encoding="utf-8") as config_file:
config.write(config_file)
To write to a different file, change the output path. Writing serializes the parser’s current representation; it does not promise to preserve every original comment, spacing choice, or formatting detail. Treat it as a configuration rewrite, not a comment-preserving editor. The output is intended to be readable again by the parser. Python 3.14 added InvalidWriteError for representations that cannot be accurately read back.
Safe update pattern: read and validate the input, update only the values your application owns, and write to a deliberate destination. If an interrupted write would be costly, write to a temporary file in the same directory and replace the target after the write succeeds.
6. Configure parser behavior deliberately
The constructor options below cover the choices most likely to affect an application. Check the Python version you support before using newer parameters.
| Setting | Default or choice | When it matters |
|---|---|---|
interpolation |
Basic interpolation by default | Use ExtendedInterpolation() for extended references, or None for literal values. |
strict |
True |
Reject duplicate sections or options within a single input source. This is usually preferable because it catches mistakes. |
allow_no_value |
False |
Enable only if the file format needs options without a value. |
delimiters |
= and : |
Choose separators accepted between option names and values. |
comment_prefixes |
# and ; |
Controls full-line comment markers. |
inline_comment_prefixes |
None | Inline comments are not enabled by default. Enabling them can make those marker characters difficult to express in values. |
empty_lines_in_values |
True |
Controls whether blank lines may continue a multiline value. |
default_section |
DEFAULT |
Changes the name used for the special defaults section. |
converters |
None | Add application-specific typed getters. |
allow_unnamed_section |
Version dependent | Added in Python 3.13; use only when supporting that behavior explicitly. |
Option names are lowercased by default. If a configuration format requires case-sensitive option names, customize optionxform() on the parser. Do this only when necessary because case-insensitive expectations are common in INI-style files.
strict=True rejects duplicates within one file, string, or dictionary input. It does not prevent layering separate files: later files can intentionally override earlier values. Do not depend on duplicate keys within one file silently replacing one another.
Indented lines can continue a multiline value. Inline comment parsing is a separate choice from full-line comments, and enabling inline prefixes can prevent literal use of those characters in values. If these details matter to your file format, define the parser options and examples together rather than relying on readers to guess.
7. Runnable end-to-end example
This script creates a sample INI file, reads it as required input, retrieves typed values, updates a setting, and writes the result. Save it as config_demo.py and run python config_demo.py.
import configparser
from pathlib import Path
path = Path("app.ini")
if not path.exists():
path.write_text(
"[DEFAULT]\\n"
"log_level = INFO\\n"
"\\n"
"[service]\\n"
"base_url = https://api.example.com\\n"
"timeout = 20\\n"
"enabled = yes\\n",
encoding="utf-8",
)
config = configparser.ConfigParser()
with path.open(encoding="utf-8") as config_file:
config.read_file(config_file)
base_url = config.get("service", "base_url")
timeout = config.getint("service", "timeout")
enabled = config.getboolean("service", "enabled")
log_level = config.get("service", "log_level")
print(f"URL: {base_url}")
print(f"Timeout: {timeout}")
print(f"Enabled: {enabled}")
print(f"Log level: {log_level}")
config["service"]["timeout"] = str(timeout + 5)
with path.open("w", encoding="utf-8") as config_file:
config.write(config_file)
The script uses the inherited log_level default when reading from service. It stores the updated integer as text because configparser writes string values.
8. Troubleshooting common errors
| Symptom | Likely cause | Fix |
|---|---|---|
Parser has no sections or expected values after read() |
The path did not exist or could not be opened; read() skips unavailable files. |
Inspect the list returned by read(), resolve the path, or use read_file() for required input. |
MissingSectionHeaderError |
Input options appear before a section header, or the file is malformed for the selected format. | Add a section header such as [service]. Python 3.13 added optional unnamed-section support for applicable files. |
DuplicateSectionError or DuplicateOptionError |
A section or option occurs more than once in the same input source while strict mode is enabled. | Remove or consolidate the duplicate. Layer separate files when intentional overrides are needed. |
NoSectionError or NoOptionError |
A requested name is absent, misspelled, or expected from the wrong section. | Check spelling and section placement; use fallback= for expected optional values. |
ValueError or conversion failure from a typed getter |
The option exists but its text is not a valid integer, float, or recognized boolean. | Correct the file value or validate it and report the accepted format. |
InterpolationMissingOptionError or another interpolation error |
A referenced option is missing, malformed, or contains a percent sequence interpreted as interpolation. | Correct the reference, escape a literal percent as %%, or use raw=True / disable interpolation. |
A value unexpectedly lacks text after # or ; |
Comment-prefix settings interpret that marker as a comment, particularly if inline comments were enabled. | Review comment_prefixes and inline_comment_prefixes; leave inline comments disabled if values need those characters. |
| Unexpected case behavior for option names | Option names are normalized to lowercase by default. | Use lowercase consistently or customize optionxform() when the format truly requires case preservation. |
| Writing changes comments or formatting | write() serializes the parsed configuration rather than preserving the source layout. |
Keep comments in documentation or use a format/tool designed for round-trip editing when preserving layout is a requirement. |
MultilineContinuationError or write-side InvalidWriteError |
Python 3.13 and 3.14 introduced these errors for particular ambiguous multiline input and unsafe-to-round-trip output cases. | Check the target Python release notes and adjust the input representation so a write can be parsed accurately. |
9. Performance, reliability, and input safety
For ordinary application configuration files, parsing is typically a small part of startup work. The practical reliability choices are more important: use explicit encodings, distinguish optional from required files, convert and validate values close to where they enter the application, and avoid rewriting a file when no update is needed.
When parsing untrusted INI data, impose an input-size limit before parsing. The Python reference warns that configparser can consume excessive CPU and memory on unbounded input. Do not treat successful parsing as schema validation: check required sections, allowed values, ranges, and cross-field rules in application code.
ConfigParser is suited to existing INI-style settings and its interpolation and layering behavior. For a newly chosen configuration format, the Python documentation also points to tomllib; TOML is a separately specified format designed as an improvement over INI. Choose based on the format your application and tooling need.
10. Or skip the browser setup
If your configuration-driven workflow also needs website screenshots, ScreenshotNeo provides a one-request screenshot API. Replace the target URL as needed; see the ScreenshotNeo API documentation for request options.
curl -G "https://api.screenshotneo.com/v1/shot" -d access_key=YOUR_API_KEY --data-urlencode url=https://stripe.com -o shot.webp
import requests
r = requests.get("https://api.screenshotneo.com/v1/shot", params={"access_key": "YOUR_API_KEY", "url": "https://stripe.com"}, timeout=90)
open("shot.webp", "wb").write(r.content)
const q = new URLSearchParams({ access_key: 'YOUR_API_KEY', url: 'https://stripe.com' });
const res = await fetch(`https://api.screenshotneo.com/v1/shot?${q}`);
ScreenshotNeo accepts cookie or consent banners like a visitor and removes more than 60 known consent platforms, newsletter popups, and chat widgets before capture; each of those steps can be turned off. Bot checks, blank pages, timeouts, failed loads, and cache hits cost nothing, and response headers identify the page verdict and billing status. Its MCP server provides take_screenshot, get_page_info, and capture_pdf tools for Claude, Cursor, and other MCP clients. The Free plan includes 1,000 screenshots per month with no card; paid plans start at $5 for 3,000 screenshots.
Create a free ScreenshotNeo account to get 1,000 screenshots a month with no card.
11. Frequently asked questions
Does configparser read JSON or YAML?
No. It reads INI-like configuration syntax. Use a parser intended for the format your file uses.
Can I preserve comments when I save a parsed file?
Do not rely on ConfigParser.write() to preserve the original comment layout or formatting. It writes the parser’s representation.
Are option names case-sensitive?
By default, option names are normalized to lowercase. Customize optionxform() only if your configuration format requires case-sensitive names.
Is successful parsing enough to trust a configuration value?
No. Parsing checks the INI structure, while your application remains responsible for required settings, valid ranges, and other rules.


