2026년 9월 6일 일요일

How do csv.writer and csv.reader round-trip commas, quotes and newlines?

csv is a module in Python's standard library; csv.writer and csv.reader are callables within that module that return writer and reader objects respectively for encoding and decoding CSV rows. Calling writerow on a writer object wraps a field containing a comma, a quotation mark, or a newline in quotation marks, and escapes a literal quotation mark inside such a field by doubling it. Iterating over a reader object reverses these transformations. Testing four string values through this write-then-read sequence showed the original values recovered unchanged.

CSV fields are not just comma splits: Comma in quotes — One CSV field; Doubled quote — One literal quote; Newline in quotes — One CSV field; 00123 — String preserved
Diagram of verified synthetic examples; details and limitations below.

The round-trip mechanism

Round-tripping means encoding a value with csv.writer and then decoding the result with csv.reader to get back the original value. csv.writer, a callable in the csv module, returns a writer object; calling writerow on that object examines each field before writing it: if the field contains a comma, a quotation mark, or a newline, writerow wraps the entire field in quotation marks, first doubling any literal quotation mark already inside it. csv.reader, the counterpart callable, returns a reader object; iterating over that object reverses this by stripping the surrounding quotation marks and converting any doubled quotation marks back into a single quotation mark.

Testing four values through this sequence — plain text, text containing a comma, text containing a quotation mark, and text containing a newline — showed each value recovered exactly as it was written, confirming the round trip for these specific cases.

Comma and newline handling

A field containing a comma is wrapped in quotation marks by csv.writer because an unquoted comma would otherwise be read as separating two fields. When csv.reader encounters that quoted field, it recognizes that the comma inside the quotation marks is part of the field's value rather than a delimiter.

A field containing a newline is handled the same way: csv.writer wraps it in quotation marks so the newline is understood as part of the field's content rather than the end of a row. In testing, a value containing a newline was correctly read back as a single field by csv.reader, rather than being split into two rows.

Quote escaping inside quoted fields

When a field's value itself contains a quotation mark, calling writerow on the writer object returned by csv.writer doubles that character and wraps the whole field in quotation marks. Iterating over the reader object returned by csv.reader detects a doubled quotation mark inside a quoted field and converts it back to a single quotation mark in the value it returns.

This doubling convention is the default escaping behavior for csv.writer and csv.reader in the csv module. In the tested case, a value containing a quotation mark was written, read back, and matched the original value exactly.

File handling and the newline setting

The csv module's documentation recommends opening files with the newline parameter set to an empty string when reading or writing CSV data, so that line-ending characters are passed through to csv.reader and csv.writer without being translated by the file layer first. This matters especially when a field's value legitimately contains a newline inside quotation marks.

The testing described here used an in-memory text buffer configured the same way, rather than an actual file on disk, so the effect of this setting on real files across different operating systems was not directly exercised. The recommendation to use this setting for files comes from the module's documentation rather than from the tests performed.

Verification through testing

To check that a round trip works, a value is passed to csv.writer, the resulting text is captured, and that text is then passed to csv.reader; the parsed result is compared to the original value. This sequence was run for the four values described earlier, and each one matched its original after being written and read back.

This kind of check is useful for confirming behavior on specific inputs. If a round trip fails for a particular value, the mismatch points to either how the file or buffer was opened or how that value interacts with the quoting and escaping behavior of csv.writer and csv.reader.

Practical considerations and limitations

These results come from running the csv module's reader and writer callables against a small set of synthetic string values in a single Python 3.12 environment, using the module's default dialect settings. Other libraries such as pandas, spreadsheet applications, and third-party CSV parsers were not tested.

Because only the default dialect was exercised, these findings should not be treated as evidence that csv.writer and csv.reader comply with any formal CSV specification, or that every CSV parser handles quoting and escaping the same way. Pairing output from csv.writer with a differently configured reader could still produce mismatched results.

Run the verified example

Python

import csv
import io

cases = ["plain", "comma, inside", 'a "quote"', "two\nlines"]
for value in cases:
    buffer = io.StringIO(newline="")
    csv.writer(buffer).writerow(["00123", value])
    encoded = buffer.getvalue()
    decoded = next(csv.reader(io.StringIO(encoded, newline="")))
    assert decoded == ["00123", value]
    print(repr(value), "round-trip:", decoded == ["00123", value])

Observed output

'plain' round-trip: True
'comma, inside' round-trip: True
'a "quote"' round-trip: True
'two\nlines' round-trip: True

Test scope and sources

Executed 2026-09-06 with Python 3.12.13 on Darwin. Tests use standard-library csv and synthetic strings. No Excel, pandas or third-party parser was executed. Do not generalize to every CSV dialect.

Read the companion article on this topic

Why does splitting a CSV row on commas give too many fields?

Splitting a CSV row with a plain string split on commas can produce more fields than the row actually contains, because commas inside quoted values are part of the data rather than delimiters between fields. The csv module in Python's standard library provides csv.reader, a callable that returns a reader object; iterating over that object parses rows while tracking whether it is inside or outside a quoted field. Testing a row containing a quoted name with an internal comma, alongside a phrase with escaped quotation marks, showed the two approaches producing different field counts from the same input.

CSV fields are not just comma splits: Comma in quotes — One CSV field; Doubled quote — One literal quote; Newline in quotes — One CSV field; 00123 — String preserved
Diagram of verified synthetic examples; details and limitations below.

The naive split problem

A simple way to parse CSV-like text is to call split on the comma character. This works when every field is free of commas, but breaks as soon as a field legitimately contains one. In the tested example, a row held three logical fields: a numeric identifier, a name that itself contained a comma, and a phrase containing escaped quotation marks. Splitting that row on commas produced four fields instead of three, because the comma inside the name was treated as if it separated two fields.

The underlying issue is that a plain split has no awareness of quoting conventions. It treats every comma as a delimiter regardless of whether that comma sits inside quotation marks meant to protect a value. Nothing in the split operation itself accounts for quoted regions of text.

How csv.reader parses differently

csv.reader, a callable provided by the csv module, returns a reader object; iterating over that object tracks state as it reads a row, distinguishing between characters that appear inside a quoted field and characters that appear outside one. A comma encountered while inside quotes is treated as literal data; a comma encountered outside quotes is treated as a field separator.

In the tested example, csv.reader parsed the same row into three fields, matching the intended structure of the data, while the naive split produced four. This difference came entirely from how each approach treated the comma inside the quoted name.

Quoted field boundaries

Wrapping a field in double quotation marks signals that the field may contain a comma without that comma being treated as a delimiter. The quotation marks mark the start and end of the field's content; they are not part of the value once parsed. A quoted field containing a comma is understood by csv.reader as a single field, even though it visually contains the delimiter character.

This is precisely the behavior a naive split lacks: it has no concept of a quoted region, so it splits on every comma it encounters, whether or not that comma is meant to be protected by surrounding quotes.

Escaped quotes inside quoted fields

In the dialect used by the csv module's default settings, a literal quotation mark that needs to appear inside a quoted field is represented by writing it twice in a row. csv.reader recognizes this doubled representation and converts it back into a single literal quotation mark in the parsed field value. The tested phrase included an escaped quotation mark, and csv.reader correctly reduced it to a single quote character in the output.

A plain split cannot perform this conversion. It would leave any doubled quotation marks in the output exactly as written, rather than interpreting them as an escape sequence for a single quote.

Comparing results between methods

Comparing the two approaches on the same input showed different field counts when a comma was protected by quotes: three fields from csv.reader against four from split. However, matching field counts between two parsing approaches does not by itself confirm that either approach parsed the row correctly. Counts can match by coincidence, or a method could produce the right number of fields with wrong content.

Verifying correctness requires checking the actual content of each field, not only how many fields resulted. Confirm that a comma expected inside one field stayed inside that field, and that any escaped quotation marks were converted to single quotes as expected.

Scope and limitations

These observations come from testing the csv module's reader callable in a single Python 3.12 run against a single synthetic example row using the module's default dialect. No testing was done against Excel-generated files, pandas, third-party CSV libraries, or non-default dialect settings.

These results should not be read as a description of the CSV format in general or as confirmation that any parser complies with a formal specification. They describe the behavior observed for the specific callable and inputs that were tested.

Run the verified example

Python

import csv
import io

line = '42,"Smith, Ada","said ""hello"""'
naive = line.split(",")
parsed = next(csv.reader(io.StringIO(line)))
assert len(naive) == 4
assert parsed == ["42", "Smith, Ada", 'said "hello"']
print("split fields:", len(naive))
print("csv fields:", len(parsed))
print(parsed)

Observed output

split fields: 4
csv fields: 3
['42', 'Smith, Ada', 'said "hello"']

Test scope and sources

Executed 2026-09-06 with Python 3.12.13 on Darwin. Tests use standard-library csv and synthetic strings. No Excel, pandas or third-party parser was executed. Do not generalize to every CSV dialect.

Read the companion article on this topic