Bobobk

Practical Python, Linux and automation guides

Validate INI Settings with Python configparser: Percent Signs, Booleans, and Duplicates

2026-10-30 Technology
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:

  1. Read one required UTF-8 file containing exactly one [task] section. Section names are case-sensitive; option names use the default case-insensitive behavior.
  2. Require explicit name, enabled, and interval options; reject unknown options. Reject options in [DEFAULT] to avoid inherited values hiding missing fields; an empty [DEFAULT] is harmless.
  3. Strip surrounding whitespace from name, then require a nonempty single line. Treat % literally. Convert enabled with getboolean(), not bool("false").
  4. Convert interval with getint() and require 1–86400 seconds. Passing validation does not mean the job is enabled: business logic must use the JSON boolean enabled.

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.

  1. 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
  1. Save the complete code below as read_task.py in 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

  1. 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}
  1. Assert that % was not interpolated, false is 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.

Tags

1024 2fa ablang adsense agents.md ai ai-agent ai-agents ai-seo algorithm amp antibodies apparmor argparse automation background backup bash batch-processing binarycif bioinformatics biopython blockchain boltz bootstrapping boxes bubblewrap c-index cca cdn checkpoint cli cloudflare codex cofoldarena concurrency configparser configuration copy cprofile cpu monitoring cron csv cuda curl data-leakage data-processing data-quality data-validation datascience datavisualization datetime debugging decimal deployment desktop-app devtools difflib disk-space disown dns docker dovecot download economics electron encoding esm esm2 esm3 esmc esmfold2 etag evaluation fable faceswap fasta fastmcp faulthandler ffmpeg file-descriptors file-io file-management filecmp filenames files flashppi flask folium frontend game generator git github-actions google google-research gpt-6.1-sol grep gzip harness heapq hls html http hugo imports indexnow ipaddress jailbreak javascript jev json jsonl just k-means kaggle langfuse leecode linux list litellm llm llms.txt logging logs lollipop lsof m3u8 machine-learning macos manacher matplotlib mcp memory mirror mmcif model-evaluation monitoring mp3 mp4 mpnn multiomics multiprocessing mutation mysql nanobert nanobodies networking networkx nginx normalize numpy ollama omegatherm pandas password pathlib pdb pep-723 performance permissions phaser pillow pip postfix preprocessing print probability prompt-engineering protein-alignment protein-design protein-embeddings protein-interactions protein-language-models protein-stability protein-structure proxy pydantic pyecharts pyqt python python3 r raincloud reproducibility requests reservoir-sampling rfoptimization rg ripgrep rosettafold3 roundcube rrsi rsi rsync s-tui sampling scale scikit-learn scrapy screen seaborn security seo sequence-identity sha256 shell signals sitemap sklearn socket solana somaticsignatures sorting spl sql sqlite ssh standardize static-site subprocess symlink sysadmin system-one tarfile tcp tempfile tensorflow tkinter tls toml totp tracemalloc trade tron tronpy troubleshooting turtle typesafe-ai ubuntu unicode upsert urllib usdt utf-8 uv venv vhhbert vite wallet web-tools webp wordcloud wordpress workflow xml yaml zipfile zoneinfo 迅雷解析

友情链接