From 92480d76ebd1f4ff2b67d741689d6dd5e0189733 Mon Sep 17 00:00:00 2001 From: Shardul D Date: Mon, 22 Jun 2026 04:00:27 +0530 Subject: [PATCH] gh-82113: Fix ConfigParser.items() returning '' for value-less options ConfigParser.items() always ran the option value through interpolation, which turned a None value (a value-less option under allow_no_value=True) into an empty string. get(), mapping access and dict(section) all return None in that case. Skip interpolation when the stored value is None so items() is consistent with the other accessors. --- Lib/configparser.py | 6 ++++-- Lib/test/test_configparser.py | 11 +++++++++++ .../2026-06-22-13-30-00.gh-issue-82113.Rb8xKt.rst | 4 ++++ 3 files changed, 19 insertions(+), 2 deletions(-) create mode 100644 Misc/NEWS.d/next/Library/2026-06-22-13-30-00.gh-issue-82113.Rb8xKt.rst diff --git a/Lib/configparser.py b/Lib/configparser.py index a53ac87276445ad..00625ebe4080489 100644 --- a/Lib/configparser.py +++ b/Lib/configparser.py @@ -908,8 +908,10 @@ def items(self, section=_UNSET, raw=False, vars=None): if vars: for key, value in vars.items(): d[self.optionxform(key)] = value - value_getter = lambda option: self._interpolation.before_get(self, - section, option, d[option], d) + value_getter = lambda option: ( + d[option] if d[option] is None + else self._interpolation.before_get(self, section, option, + d[option], d)) if raw: value_getter = lambda option: d[option] return [(option, value_getter(option)) for option in orig_keys] diff --git a/Lib/test/test_configparser.py b/Lib/test/test_configparser.py index 8d8dd2a2bf27fbf..c164fec6bd535dc 100644 --- a/Lib/test/test_configparser.py +++ b/Lib/test/test_configparser.py @@ -1327,6 +1327,17 @@ def test_other_errors(self): class ConfigParserTestCaseNoValue(ConfigParserTestCase): allow_no_value = True + def test_items_reports_none_for_no_value(self): + # gh-82113: items() must report None (not '') for value-less options, + # consistent with get(), mapping access and dict(section). + cf = self.fromstring("[s]\nno_value\nspam = ham\n") + self.assertIsNone(cf.get("s", "no_value")) + self.assertIsNone(cf["s"]["no_value"]) + self.assertEqual(sorted(cf.items("s")), + [("no_value", None), ("spam", "ham")]) + self.assertEqual(sorted(cf.items("s", raw=True)), + [("no_value", None), ("spam", "ham")]) + class NoValueAndExtendedInterpolation(CfgParserTestCaseClass): interpolation = configparser.ExtendedInterpolation() diff --git a/Misc/NEWS.d/next/Library/2026-06-22-13-30-00.gh-issue-82113.Rb8xKt.rst b/Misc/NEWS.d/next/Library/2026-06-22-13-30-00.gh-issue-82113.Rb8xKt.rst new file mode 100644 index 000000000000000..773fe701edae891 --- /dev/null +++ b/Misc/NEWS.d/next/Library/2026-06-22-13-30-00.gh-issue-82113.Rb8xKt.rst @@ -0,0 +1,4 @@ +:meth:`configparser.ConfigParser.items` now reports ``None`` rather than an +empty string for value-less options when ``allow_no_value`` is enabled, making +it consistent with :meth:`~configparser.ConfigParser.get`, mapping access and +``dict()`` conversion of a section.