csv.reader() Crashes with a Wrong Text-Mode Error on None (tested)

Code that reads lines from a non-blocking source (a socket, a queue, a custom generator) often has a moment where there's "nothing ready yet" and yields None to signal a retry. We measured that feeding such an iterable directly to csv.reader() is not safe: a single None value crashes with _csv.Error: iterator should return strings, not NoneType (the file should be opened in text mode) -- an error that looks like a file-mode mistake, even though no file is involved and valid CSV rows are sitting right behind the None in the same iterable.

1. A real, valid CSV row behind one None from the source iterable

import csv

class FlakyLines:
    def __init__(self, lines):
        self.lines = lines
        self.i = 0
    def __iter__(self):
        return self
    def __next__(self):
        if self.i == 0:
            self.i += 1
            return None          # transient: no line ready yet
        if self.i - 1 < len(self.lines):
            line = self.lines[self.i - 1]
            self.i += 1
            return line
        raise StopIteration

try:
    for row in csv.reader(FlakyLines(["a,b,c", "1,2,3"])):
        print("row:", row)
except Exception as e:
    print("CRASHED:", type(e).__name__, e)
CRASHED: Error iterator should return strings, not NoneType (the file should be opened in text mode)

Measured: the real rows ("a,b,c", "1,2,3") are valid and waiting, but csv.reader() crashes on the very first None it sees. The error message talks about text mode and file objects -- there is no file here at all, just a plain iterable.

2. The fix: filter None out of your own iterable before csv.reader sees it

def retrying_lines(raw_lines):
    for line in raw_lines:
        if line is None:
            continue              # retry: not ready yet, skip it
        yield line

raw_lines = [None, "a,b,c", "1,2,3"]
for row in csv.reader(retrying_lines(raw_lines)):
    print("row:", row)
row: ['a', 'b', 'c']
row: ['1', '2', '3']

Measured: filtering out the retry signal before it reaches csv.reader() parses both rows correctly.

Frequently asked questions

Is this documented behavior?

The csv module's documentation for reader() says the argument can be any object which supports the iterator protocol and returns a string each time its __next__() method is called; it does not mention None or what happens if that contract is violated. We measured the resulting error message, which misleadingly suggests a file text-mode problem.

Why does the error mention text mode when there's no file?

We measured that csv's C implementation uses the same internal check it uses to catch the genuine mistake of iterating a file opened in binary mode (where lines come back as bytes, not str); None fails that same type check and triggers the identical message, which was written assuming the only way to get a non-str value is a file-mode mistake.

Is this likely to come up in real code?

Yes -- any code that reads CSV lines from a non-blocking source (a socket, a message queue, a custom line-buffering generator) and uses the common while chunk := source.get_line(): pattern where None means 'not ready yet' is at risk if that None ever reaches csv.reader() directly instead of being filtered out first.

Is this related to the other None/EOF findings?

Yes -- same family already confirmed in shutil.copyfileobj, gzip/bz2/lzma, tarfile/zipfile, TextIOWrapper.readline(), ElementTree.parse(), tomllib.load(), email.message_from_binary_file(), plistlib.load(), wave.open(), json.load(), http.client.HTTPResponse, smtplib.getreply(), poplib._getline(), and base64.encode()/decode(): a None meant as a retry signal gets mishandled one layer above where it originated. This is the first case triggered by a plain custom iterable rather than anything built on io.BufferedReader.

Written with AI assistance. Measured on 2026-10-09 with Python 3.13.5's built-in csv.reader(), iterating a plain Python generator whose first yielded value is None (simulating a retry signal from a non-blocking line source) before yielding real, valid CSV lines. PlainHeaders is our extension; Python's csv module is built in.