馃 Written by Claude Opus 5.5 via Claude Code for the iniconfig maintainers; I prompted it, it did the work, I read it.
Why this matters
pytest reads pytest.ini, tox.ini and setup.cfg through iniconfig.IniConfig(path) (_pytest/config/findpaths.py). It turns ParseError into a UsageError and requires iniconfig>=2 with no upper bound. Two consequences follow:
- Every behaviour change to the constructor reaches all pytest users on upgrade.
tox.ini and setup.cfg are also read by tox, setuptools and flake8 through configparser. Wherever iniconfig and configparser disagree, one file means two different things.
To find where they disagree, iniconfig 2.3.1 was run against 44 edge-case files and compared with configparser (ConfigParser and RawConfigParser), configobj, Node ini, PHP parse_ini_string, Go gopkg.in/ini.v1 and rust-ini.
Bug 1: malformed section headers are silently swallowed (constructor and parse())
_parseline treats any line that starts with [ but does not end with ] as continuation data, even when the line is not indented:
[s]
k = v
[t] extra
j = w
- iniconfig:
{"s": {"k": "v\n[t] extra", "j": "w"}}. Section t disappears and j moves into s.
- configparser:
{"s": {"k": "v"}, "t": {"j": "w"}}
With [t (no closing bracket), iniconfig swallows the line the same way, while configparser raises ParsingError. As the first line of a file, [s produces the misleading error "unexpected value continuation".
Here is a pytest-shaped reproduction of the typo:
# tox.ini
[testenv]
commands = pytest
[pytest
addopts = -x
markers =
slow: slow tests
pytest finds no [pytest] section and silently runs without this configuration, so --strict-markers then fails on slow. tox rejects the same file with ParsingError.
Proposal: raise ParseError for a line that starts with [ and is not a valid section header. The messages would be "unclosed section header" and "unexpected text after section header". For pytest this turns silently ignored configuration into a clear UsageError. The only files it breaks are ones whose meaning is already corrupted.
Bug 2: parse() cuts values at # or ; anywhere
IniConfig.parse() strips inline comments by default, but it does not require whitespace before the comment character:
| value |
parse() |
configparser(inline_comment_prefixes=("#", ";")) |
a#b |
a |
a#b |
http://x/#frag |
http://x/ |
http://x/#frag |
d;e (continuation line) |
d |
d;e |
-k 'a or b' # c |
-k 'a or b' |
-k 'a or b' |
pytest is not affected today, because it uses the constructor, which does not strip inline comments. This has to be fixed before pytest could ever move to parse(), though. Values such as filterwarnings messages, URLs and marker descriptions with ; would otherwise be cut short.
Proposal: only treat # or ; as a comment marker when whitespace comes before it, matching configparser.
Proposed policy
iniconfig reads like RawConfigParser(strict=True) unless a difference is documented. An undocumented difference is a bug.
Under that policy, the current behaviour lines up as follows:
| Topic |
iniconfig |
configparser |
Note for pytest |
Whitespace in section names ([ s ], NO-BREAK SPACE) |
kept, opt-in strip via parse(strip_section_whitespace=True) |
kept |
Same. Optionally warn, which was the original ask in #4. |
| Unicode whitespace around keys and values |
stripped |
stripped |
Same. |
| Duplicate section or key |
error |
error |
Same. |
| Key case |
case-sensitive |
lowercased |
Document it. pytest ini keys are case-sensitive. |
Indented first key ( k = v right after a header) |
error |
accepted |
Stricter but loud. Keep it, maybe with a clearer message. |
| Blank line inside a continuation |
dropped |
kept |
Document it. pytest's line-list values are unaffected. |
| UTF-8 BOM |
stripped |
error |
Keep it (#76). |
Indented [t] |
continuation |
continuation |
Same. A warning for a line that looks like a header would be cheap. |
Interpolation, quotes, [DEFAULT] |
none, kept, ordinary section |
% interpolation (ConfigParser), kept, inherited |
Document it. pytest behaves like RawConfigParser. |
Separately, the strip_section_whitespace docstrings in __init__.py and _parse.py say "section and key names". The flag only affects section names, and keys are always stripped.
Questions
- Should bug 1 raise an error in the constructor right away? It reaches pytest users without a pin. Alternatively, it could warn for one release first.
- Should bug 2 be fixed in a 2.3.x patch release, given that
parse() shipped in 2.3.0?
馃 Written by Claude Opus 5.5 via Claude Code for the iniconfig maintainers; I prompted it, it did the work, I read it.
Why this matters
pytest reads
pytest.ini,tox.iniandsetup.cfgthroughiniconfig.IniConfig(path)(_pytest/config/findpaths.py). It turnsParseErrorinto aUsageErrorand requiresiniconfig>=2with no upper bound. Two consequences follow:tox.iniandsetup.cfgare also read by tox, setuptools and flake8 throughconfigparser. Wherever iniconfig andconfigparserdisagree, one file means two different things.To find where they disagree, iniconfig 2.3.1 was run against 44 edge-case files and compared with
configparser(ConfigParserandRawConfigParser), configobj, Nodeini, PHPparse_ini_string, Gogopkg.in/ini.v1andrust-ini.Bug 1: malformed section headers are silently swallowed (constructor and
parse())_parselinetreats any line that starts with[but does not end with]as continuation data, even when the line is not indented:{"s": {"k": "v\n[t] extra", "j": "w"}}. Sectiontdisappears andjmoves intos.{"s": {"k": "v"}, "t": {"j": "w"}}With
[t(no closing bracket), iniconfig swallows the line the same way, while configparser raisesParsingError. As the first line of a file,[sproduces the misleading error "unexpected value continuation".Here is a pytest-shaped reproduction of the typo:
pytest finds no
[pytest]section and silently runs without this configuration, so--strict-markersthen fails onslow. tox rejects the same file withParsingError.Proposal: raise
ParseErrorfor a line that starts with[and is not a valid section header. The messages would be "unclosed section header" and "unexpected text after section header". For pytest this turns silently ignored configuration into a clearUsageError. The only files it breaks are ones whose meaning is already corrupted.Bug 2:
parse()cuts values at#or;anywhereIniConfig.parse()strips inline comments by default, but it does not require whitespace before the comment character:parse()configparser(inline_comment_prefixes=("#", ";"))a#baa#bhttp://x/#fraghttp://x/http://x/#fragd;e(continuation line)dd;e-k 'a or b' # c-k 'a or b'-k 'a or b'pytest is not affected today, because it uses the constructor, which does not strip inline comments. This has to be fixed before pytest could ever move to
parse(), though. Values such asfilterwarningsmessages, URLs and marker descriptions with;would otherwise be cut short.Proposal: only treat
#or;as a comment marker when whitespace comes before it, matchingconfigparser.Proposed policy
iniconfig reads like
RawConfigParser(strict=True)unless a difference is documented. An undocumented difference is a bug.Under that policy, the current behaviour lines up as follows:
[ s ], NO-BREAK SPACE)parse(strip_section_whitespace=True)k = vright after a header)[t][DEFAULT]%interpolation (ConfigParser), kept, inheritedRawConfigParser.Separately, the
strip_section_whitespacedocstrings in__init__.pyand_parse.pysay "section and key names". The flag only affects section names, and keys are always stripped.Questions
parse()shipped in 2.3.0?