configparser.read_file() Stringifies a None Line as Text (tested)
read_file() accepts any iterable of lines, not just a real file -- a common choice when lines come from a generator, a queue, or a non-blocking source that can emit a transient None sentinel for "nothing ready yet." We measured that read_file() never checks: each line is wrapped in configparser's internal _Line(str) subclass via str.__new__(cls, val), which is exactly what str(None) does -- it silently turns the None object into the three-character string 'None', with no exception at the point of conversion. That fabricated text then gets parsed as if it were real file content, and the resulting ParsingError or MissingSectionHeaderError quotes 'None' as the offending line -- making it look like the literal word "None" was typed into the config, when actually a sentinel value reached a function that only accepts real text lines.
1. A real, valid config with one None line in the middle
import configparser
class FlakySource:
"""Yields real config lines, but one read returns None -- the same
kind of transient sentinel a non-blocking line source can produce."""
def __init__(self, lines):
self.lines = lines
self.i = 0
def __iter__(self):
return self
def __next__(self):
if self.i >= len(self.lines):
raise StopIteration
v = self.lines[self.i]
self.i += 1
return v
payload = ["[server]\n", None, "host = example.com\n", "port = 8080\n"]
cp = configparser.ConfigParser()
try:
cp.read_file(FlakySource(payload))
print("parsed OK:", cp.sections())
except Exception as e:
print("CRASHED:", type(e).__name__, "--", e)
CRASHED: ParsingError -- Source contains parsing errors: '<???>'
[line 2]: 'None'
Measured: the config is valid -- three real lines around one sentinel. read_file() wraps every line in _Line(str), whose __new__ calls str.__new__(cls, val) -- the same thing str(None) does. The None becomes the text 'None' before any parsing logic ever runs, so the error message reports a line of actual file content that was never in the source.
2. The fix: filter None out of the line source before read_file() sees it
def skip_none_lines(source):
for line in source:
if line is None:
continue # retry: a sentinel, not a real line
yield line
cp2 = configparser.ConfigParser()
cp2.read_file(skip_none_lines(FlakySource(payload)))
print("parsed OK:", cp2.sections(), dict(cp2["server"]))
parsed OK: ['server'] {'host': 'example.com', 'port': '8080'}
Measured: dropping None before it reaches read_file() parses the same lines correctly. If a ParsingError or MissingSectionHeaderError quotes a line that reads exactly 'None' and you know you never wrote that into the file, the real cause is almost certainly a line source that occasionally yields the None object itself, not text containing the word.
Frequently asked questions
Is this documented behavior?
configparser's documentation says read_file() takes an iterable yielding Unicode strings, such as the lines of a file. It does not say what happens if an item isn't a string. We measured that a None item is accepted without error at the point it's read -- it's silently coerced by _Line's str.__new__(cls, val) call, which behaves exactly like str(None).
Why does the error say 'None' instead of raising a clean TypeError?
We measured that configparser never calls isinstance() or any type check on the incoming lines -- it wraps each one directly in its internal _Line(str) subclass. str.__new__(cls, None) doesn't raise; it returns the string 'None', just like calling str(None) would. By the time any parsing logic runs, the None object is already gone, replaced by three characters of text.
Does this depend on where the None line appears?
We measured two different exception types depending on position: a None before any [section] header raises MissingSectionHeaderError (since 'None' doesn't look like a section line), while a None after a section header raises ParsingError. Both quote the fabricated 'None' text as the problem line; neither hints that a non-string value was the actual cause.
Is this related to the other None/EOF findings?
It's in the same family as the None-sentinel findings already confirmed in json.load(), csv.reader(), shutil.copyfileobj, and others, but it's a different flavor: those crash or silently drop data when a stream's None is mishandled. configparser.read_file() does neither -- it successfully converts the None into real-looking text and lets parsing continue, which is more likely to send you looking for a typo in your file instead of a None leaking out of your line source.
Written with AI assistance. Measured on 2026-10-09 with Python 3.13.5's built-in configparser.ConfigParser.read_file(), feeding it a custom line iterator (not a real file) whose sequence of real, valid config lines includes one None in the middle. PlainHeaders is our extension; Python's configparser module is built in.