csv QUOTE_NONNUMERIC Booleans Round-Trip Failure (tested)

csv.QUOTE_NONNUMERIC is meant to quote every field except actual numbers, so a reader using the same setting can safely parse unquoted fields as floats. We measured what happens when one of your values is a Python bool: the writer treats it as a number, and the matching reader can't read its own output back.

1. The writer leaves True/False unquoted

import csv, io

buf = io.StringIO()
w = csv.writer(buf, quoting=csv.QUOTE_NONNUMERIC)
w.writerow([1, "text", True, None, 3.14])

buf.getvalue()
'1,"text",True,"",3.14\r\n'

Measured: "text" and the empty string from None are quoted, as expected under QUOTE_NONNUMERIC. But True comes out completely unquoted, written as the literal word True — not the number 1, and not quoted like the string fields. No error or warning is raised; the writer considers this correct output.

2. Reading the same file back with the same setting crashes

buf.seek(0)
r = csv.reader(buf, quoting=csv.QUOTE_NONNUMERIC)
list(r)
Traceback (most recent call last):
  ...
ValueError: could not convert string to float: 'True'

Measured: csv.reader with QUOTE_NONNUMERIC tries to float() every unquoted field — that's the whole point of the setting. Since the writer left True unquoted, the reader tries float("True") and raises. The file the writer produced cannot be read back by the exact matching reader configuration that's supposed to round-trip it.

3. Why: Python's bool is a subclass of int

Per the csv docs, QUOTE_NONNUMERIC tells the writer to leave a field unquoted when it's an instance of a numeric type. Because bool is a subclass of int in Python (isinstance(True, int) is True), the writer's numeric check passes for booleans too — but it writes the field using str(value), which turns True into the word "True", not the digit "1". The writer's "is this numeric" check and its "how do I render this" step use different logic, and nothing reconciles them for the one type where that distinction matters.

4. Fix: convert bools to int before writing under QUOTE_NONNUMERIC

buf2 = io.StringIO()
w2 = csv.writer(buf2, quoting=csv.QUOTE_NONNUMERIC)
w2.writerow([1, "text", int(True), None, 3.14])

buf2.getvalue()
'1,"text",1,"",3.14\r\n'

buf2.seek(0)
list(csv.reader(buf2, quoting=csv.QUOTE_NONNUMERIC))
[[1.0, 'text', 1.0, '', 3.14]]

Measured: explicitly converting any boolean values to int before passing them to writerow() produces 1 instead of True, which both writes and reads back cleanly under QUOTE_NONNUMERIC (note the round trip turns it into 1.0, a float, not 1 — a separate, already-documented quirk of this quoting mode). If you don't control every value going into a row, don't assume QUOTE_NONNUMERIC output is readable by the matching reader setting without checking for stray booleans first.

Frequently asked questions

Why does csv.writer with QUOTE_NONNUMERIC write True/False without quotes?

We measured it because Python's bool type is a subclass of int, so QUOTE_NONNUMERIC's 'is this a number' check treats booleans as numeric and leaves them unquoted — but it still writes the word True/False via str(), not the digit 1/0.

Why does reading a CSV with QUOTE_NONNUMERIC fail on a field that says True?

We confirmed csv.reader(quoting=QUOTE_NONNUMERIC) calls float() on every unquoted field, and float('True') raises ValueError — this happens specifically when the file was written with an unconverted Python bool under the same quoting mode.

Does csv.writer raise any error when writing a bool under QUOTE_NONNUMERIC?

No — we measured the write succeeding silently with no exception or warning. The resulting file looks fine until something tries to read it back with QUOTE_NONNUMERIC set.

How do I fix booleans breaking CSV round-trips under QUOTE_NONNUMERIC?

We verified that converting booleans to int (e.g. int(True) -> 1) before calling writerow() produces output that both writes correctly and reads back without error under the same QUOTE_NONNUMERIC setting.

Does this same problem happen with QUOTE_MINIMAL or QUOTE_ALL?

We didn't measure those directly here, but the failure is specific to QUOTE_NONNUMERIC's reader-side float() conversion — QUOTE_MINIMAL and QUOTE_ALL readers don't attempt any type conversion, so an unquoted or quoted 'True' string reads back as the plain string 'True' without error.

Written with AI assistance. Measured on 2026-10-03 with Python 3.13.5's built-in csv module: wrote a row containing a Python bool under csv.QUOTE_NONNUMERIC, inspected the raw output bytes, then read that same file back with csv.reader(quoting=QUOTE_NONNUMERIC). PlainHeaders is our extension; Python's csv module is built in.