Validate INI Settings with Python configparser: Percent Signs, Booleans, and Duplicates
On this page
A task name contains 100%, but reading its INI file raises InterpolationSyntaxError. Another script runs even though the file says enabled = false. Small automation jobs need explicit parsing rules instead of passing raw strings into business logic.
This beginner-to-intermediate tutorial builds a read-only INI configuration checker with Python’s standard library: preserve literal percent signs, convert booleans and integers, and reject duplicates, typos, and missing files. It prints validated settings only; it does not launch jobs or edit schedulers or system configuration.
1. Method: define the configuration contract
Projects already using TOML can keep the tomllib task configuration approach. This article addresses small scripts that still use INI, without treating it as TOML or migrating formats automatically.
Our policy is:
- Read one required UTF-8 file containing exactly one
[task]section. Section names are case-sensitive; option names use the default case-insensitive behavior. - Require explicit
name,enabled, andintervaloptions; reject unknown options. Reject options in[DEFAULT]to avoid inherited values hiding missing fields; an empty[DEFAULT]is harmless. - Strip surrounding whitespace from
name, then require a nonempty single line. Treat%literally. Convertenabledwithgetboolean(), notbool("false"). - Convert
intervalwithgetint()and require 1–86400 seconds. Passing validation does not mean the job is enabled: business logic must use the JSON booleanenabled.
The official interpolation documentation describes default % interpolation. Setting interpolation=None disables it, so even %(other)s stays literal rather than becoming a variable reference.
2. Setup: create an isolated demo directory
Use an existing Python 3.9 or newer interpreter; no third-party packages are needed. Commands were tested on Linux with Python 3.11.4 in bash/zsh.
- Run the following in one terminal. Choose a fresh directory name when repeating the experiment to avoid overwriting old fixtures:
python3 --version
mkdir ini-task-demo
cd ini-task-demo
cat > task.ini <<'INI'
[task]
name = nightly-100%
enabled = false
interval = 60
INI
- Save the complete code below as
read_task.pyin the current directory. These settings contain no passwords; do not print credential-bearing configuration into real logs.
3. Implementation: required files, strict parsing, and typed values
"""Validate one small local INI task file without running the task. Python 3.9+."""
import argparse
import configparser
import json
from pathlib import Path
import sys
def load_task(path):
parser = configparser.ConfigParser(interpolation=None, strict=True)
with path.open(encoding="utf-8") as stream:
parser.read_file(stream)
if parser.defaults():
raise ValueError("DEFAULT values are not allowed")
if parser.sections() != ["task"]:
raise ValueError("expected exactly one [task] section")
section = parser["task"]
required = {"name", "enabled", "interval"}
unknown = set(section) - required
missing = required - set(section)
if unknown or missing:
raise ValueError(
f"unknown keys: {sorted(unknown)}; missing keys: {sorted(missing)}"
)
name = section["name"].strip()
if not name or "\n" in name or "\r" in name:
raise ValueError("name must be nonempty and single-line")
enabled = section.getboolean("enabled")
interval = section.getint("interval")
if not 1 <= interval <= 86400:
raise ValueError("interval must be between 1 and 86400 seconds")
return {"name": name, "enabled": enabled, "interval": interval}
def main():
cli = argparse.ArgumentParser(description=__doc__, allow_abbrev=False)
cli.add_argument("config", type=Path, help="UTF-8 INI file to validate")
args = cli.parse_args()
try:
task = load_task(args.config)
except (OSError, UnicodeError, configparser.Error, ValueError) as exc:
print(f"ERROR: {exc}", file=sys.stderr)
return 1
print(json.dumps(task, ensure_ascii=False))
return 0
if __name__ == "__main__":
raise SystemExit(main())
Use open() followed by read_file() for required input. Missing files fail instead of being ignored by read(). Strict parsing rejects duplicate sections/options in this one source; our checker does not merge files. Checking defaults first prevents inherited values from satisfying required fields.
getboolean() accepts yes/no, on/off, true/false, and 1/0, case-insensitively. Check the integer range after conversion. Neither conversion runs the task.
4. Usage: verify output and exit codes
- View help and check the configuration:
python3 read_task.py --help
python3 read_task.py task.ini
Expected output:
{"name": "nightly-100%", "enabled": false, "interval": 60}
- Assert that
%was not interpolated,falseis not a string, and the interval is an integer:
python3 - <<'PY_CHECK'
import json
import subprocess
import sys
result = subprocess.run(
[sys.executable, "read_task.py", "task.ini"],
capture_output=True, text=True, check=False,
)
assert result.returncode == 0, result.stderr
assert result.stderr == ""
assert json.loads(result.stdout) == {
"name": "nightly-100%", "enabled": False, "interval": 60,
}
print("PASS: literal percent, boolean false, integer interval")
PY_CHECK
Success returns 0. File, parsing, or field errors return 1 with stderr and empty stdout. CLI argument errors return 2. enabled = false is valid and still returns 0; callers must inspect the parsed boolean before deciding whether to execute a job.
If the job also uses environment variables, follow Python environment variable validation and define source precedence rather than using string truthiness.
5. Validation: deliberately break the configuration
This test creates invalid copies in the demo directory without changing task.ini. Re-running it overwrites fixtures with the same names:
python3 - <<'PY_TEST'
from pathlib import Path
import subprocess
import sys
base = Path("task.ini").read_text(encoding="utf-8")
cases = [
("duplicate.ini", base + "enabled = true\n", "already exists"),
("typo.ini", base.replace("interval =", "intervl ="), "unknown keys"),
("boolean.ini", base.replace("false", "maybe"), "Not a boolean"),
("range.ini", base.replace("interval = 60", "interval = 0"), "between 1"),
("defaults.ini", "[DEFAULT]\nenabled = true\n" + base, "DEFAULT values"),
("missing-key.ini", base.replace("enabled = false\n", ""), "missing keys"),
]
for name, text, message in cases:
path = Path(name)
path.write_text(text, encoding="utf-8")
before = path.read_bytes()
result = subprocess.run(
[sys.executable, "read_task.py", name],
capture_output=True, text=True, check=False,
)
assert result.returncode == 1, result
assert result.stdout == "", result.stdout
assert message in result.stderr, result.stderr
assert path.read_bytes() == before
print(f"PASS: {name}")
missing = Path("not-created.ini")
assert not missing.exists()
result = subprocess.run(
[sys.executable, "read_task.py", str(missing)],
capture_output=True, text=True, check=False,
)
assert result.returncode == 1 and not result.stdout and result.stderr
assert not missing.exists()
print("PASS: required file missing")
PY_TEST
Expect seven PASS lines. Failures must not print a success report, input bytes must stay unchanged, and checking a missing file must not create it. Also test empty input, encoding errors, duplicate sections, and range boundaries before connecting business logic.
6. Troubleshooting: fix the reported cause
| Symptom | Cause and fix |
|---|---|
% raises InterpolationSyntaxError |
The running code still creates a default parser. Use ConfigParser(interpolation=None, strict=True). Do not change % to %% in this literal mode: both signs would be preserved. |
Not a boolean |
Use a supported value such as enabled = false on its own line, not enabled = false # disabled. This checker does not enable inline comments. |
already exists |
Remove duplicate options, including case variants such as enabled and Enabled. Do not hide ambiguity with strict=False. |
DEFAULT values are not allowed |
Move needed options into [task], then remove default options. |
| Configuration file not found | Fix the path or pass an absolute path. See Linux scheduled-job PATH and cwd troubleshooting for scheduler working-directory issues. |
missing keys or unknown keys |
Correct missing or misspelled name, enabled, and interval instead of adding silent fallbacks. |
INI is not a shell: this example does not expand $HOME or execute commands, and quotes around values are not automatically removed. Put comments on separate lines. The checker is for small local settings you control, not resource isolation for hostile large inputs.
7. Summary
Parsing an INI file is only the first step: check file presence, required fields, types, and ranges before doing work. Literal mode fixes percent interpolation while strict parsing exposes duplicates and typos. Verify both passing settings and failure exits, then integrate load_task() into the actual automation job.
- Author: yanzhi
- Original article: https://www.bobobk.com/en/python-configparser-ini-validation.html
- License: This work is licensed under CC BY-NC-ND 4.0. See the license for attribution, noncommercial use and no-derivatives terms.
Related articles
- Compare Configuration Files with Python: Unified Diffs, Line Endings, and Exit Codes
- Wait for a File in Python: Timeouts, Atomic Handoffs, and Exit Codes
- Python File Permissions on Linux: umask, Mode 600, and No Overwrites
- Merge Sorted Logs by Timestamp in Python with heapq.merge
- Read the Last N Log Lines in Python with deque