csv.Error: need to escape, but no escapechar set (tested)
csv.writer(f, quoting=csv.QUOTE_NONE) raises _csv.Error: need to escape, but no escapechar set the moment any field contains the delimiter — the mode that turns off quoting entirely still needs a way to represent a literal comma, and by default it has none. We measured exactly when this fires and what fixes it.
1. QUOTE_NONE with a field containing the delimiter
import csv, io
f = io.StringIO()
w = csv.writer(f, quoting=csv.QUOTE_NONE)
w.writerow(["a,b", "c"])
_csv.Error: need to escape, but no escapechar set
Measured: QUOTE_NONE disables quoting completely, so csv.writer has no way to mark "a,b" as one field rather than two — it needs to escape the embedded comma instead, and by default there's no escape character configured to do that, so it refuses outright rather than writing ambiguous output.
2. Setting escapechar fixes it, with a visible cost
w = csv.writer(f, quoting=csv.QUOTE_NONE, escapechar="\\")
w.writerow(["a,b", "c"])
# writes: a\,b,c
Measured: with escapechar="\\" set, the embedded comma is backslash-escaped in the output instead of raising. The same applies to the quote character itself (a"b becomes a\"b) — anything that would otherwise be ambiguous gets a backslash in front of it. This is non-standard CSV; most other tools and csv.reader's own default dialect don't expect backslash escapes, so round-tripping this output back through a default-configured reader will not undo the escaping correctly.
3. QUOTE_ALL and QUOTE_NONNUMERIC don't have this problem
csv.writer(f, quoting=csv.QUOTE_ALL).writerow(["a,b", "c"])
# writes: "a,b","c" -- no error, no escapechar needed
Measured: both QUOTE_ALL and the default QUOTE_MINIMAL wrap the field in quotes instead, which unambiguously contains the embedded delimiter without needing an escape character at all. QUOTE_NONE is the one mode where this entire problem exists, because it's the one mode that refuses to use quotes as the solution.
4. A separate, unrelated gotcha: QUOTE_NONNUMERIC always reads as float, never int
f2 = io.StringIO(\'"a",1,2.5\n\')
row = list(csv.reader(f2, quoting=csv.QUOTE_NONNUMERIC))[0]
[(v, type(v).__name__) for v in row]
[(\'a\', \'str\'), (1.0, \'float\'), (2.5, \'float\')]
Measured: with QUOTE_NONNUMERIC set on a reader, every unquoted field is converted with float() — the clean-looking integer 1 comes back as 1.0, a float, never an int. Code that checks isinstance(value, int) on data read this way will find every number fails that check, and an unquoted field that isn't valid float syntax (e.g. a stray unquoted word) raises ValueError: could not convert string to float instead of anything CSV-specific.
5. What to actually do
For writing, don't reach for QUOTE_NONE unless you specifically need delimiter-free, unquoted output and control both ends of the pipe — the default QUOTE_MINIMAL already quotes only the fields that need it, with no escapechar required. If you do need QUOTE_NONE, set escapechar explicitly and don't expect a standard CSV reader elsewhere to undo the backslash escaping. For reading with QUOTE_NONNUMERIC, convert to int yourself afterward wherever you actually need an integer type, rather than relying on the reader to preserve it.
Frequently asked questions
Why does csv.writer raise 'need to escape, but no escapechar set'?
We measured that this fires specifically under quoting=csv.QUOTE_NONE when a field contains the delimiter or quote character — QUOTE_NONE disables quoting entirely, so without an escapechar configured there's no way to represent that character unambiguously, and csv.writer refuses rather than writing broken output.
How do I fix 'need to escape, but no escapechar set'?
We confirmed setting escapechar (e.g. csv.writer(f, quoting=csv.QUOTE_NONE, escapechar='\\\\')) fixes it by backslash-escaping the problem character instead of raising — or switch away from QUOTE_NONE to the default QUOTE_MINIMAL, which quotes the field instead and needs no escapechar at all.
Do QUOTE_ALL or QUOTE_MINIMAL need an escapechar too?
No — we measured that only QUOTE_NONE has this requirement, since it's the only mode that refuses to wrap a field in quotes; QUOTE_ALL and QUOTE_MINIMAL both just quote the field instead, which handles an embedded delimiter with no escapechar needed.
Does csv.reader with QUOTE_NONNUMERIC return integers for whole numbers?
No — we measured that every unquoted field is converted with float(), so even a clean integer-looking field like 1 comes back as the float 1.0, never an int, regardless of how it was written.
What happens if an unquoted field isn't a valid number under QUOTE_NONNUMERIC?
We confirmed it raises ValueError: could not convert string to float, with the specific string included — not a csv-specific exception.
Written with AI assistance. Measured on 2026-10-03 with Python 3.13.5's built-in csv module: wrote rows containing the delimiter and quote characters under csv.QUOTE_NONE with and without escapechar set, and separately measured csv.QUOTE_NONNUMERIC's read-side type conversion on clean integer-looking unquoted fields. PlainHeaders is our extension; Python's csv module is built in.