Python CSV output has blank rows: let csv handle the newline boundary
By ON Lab · Sources checked November 8, 2026
When using Python 3's csv reader or writer with a text file, open the file with newline="" so the csv module handles its own newline conventions. Extra blank rows can occur when a writer's row ending is translated again by the text layer. Check the actual bytes and parsed records before deleting blank lines from the output.
A historical reader reported blank rows between CSV records in spreadsheet and text viewers after writing through the default text-file opening mode. We checked current Python csv documentation and compared two explicit text-wrapper settings locally. The CRLF translation in our test was deliberately configured on macOS; we did not run Windows or reproduce the reader's spreadsheet.
Use the Python 3 text interface
import csv
rows = [["id", "note"], ["1", "first line\nsecond line"]]
with open("csv-check-new.csv", "x", newline="", encoding="utf-8") as output:
csv.writer(output).writerows(rows)
with open("csv-check-new.csv", newline="", encoding="utf-8") as source:
restored = list(csv.reader(source))
assert restored == rows
The example chooses a new filename and exclusive creation so it does not overwrite an existing export. It uses a real newline character inside one field, expressed as a Python escape in the string. The csv writer is responsible for quoting that field correctly. The assertion compares logical records, not a count of physical text lines.
Do not copy old Python 2 binary-file recipes into a Python 3 csv workflow without checking the version. Python 3's standard csv interface works with strings in the documented text-file examples. Explicit encoding and newline handling are separate choices.
What our text-layer comparison showed
We wrote two original records through csv.writer into an in-memory byte buffer wrapped in TextIOWrapper. One wrapper used newline="". The other explicitly translated line feeds into CRLF, modeling that output conversion without claiming to be a Windows machine.
| Text wrapper | Row-ending bytes | Parsed records |
|---|---|---|
| No output newline translation | One CR followed by LF | The two original records |
| Explicit CRLF translation | Doubled CR before LF at row boundaries | Extra empty records appeared |
The untranslated case preserved the embedded newline in the note field and round-tripped to the original two records. In the translated case, the writer's existing carriage return was followed by another carriage return from the text layer's line-feed conversion. Reading the produced text showed extra empty records.
This verifies the mechanism in the controlled buffer. It does not prove that every viewer showing spacing has doubled carriage returns, or that every blank record in a real export was unintended.
Inspect bytes and logical records separately
from pathlib import Path
import csv
print(repr(Path("csv-check-new.csv").read_bytes()))
with open("csv-check-new.csv", newline="", encoding="utf-8") as source:
for number, row in enumerate(csv.reader(source), start=1):
print(number, repr(row))
For a small non-confidential sample, the bytes representation makes carriage returns and line feeds visible. A normal CRLF ending appears as \r\n; a doubled carriage return appears as \r\r\n. A terminal or editor can display those control characters differently, so appearance alone is incomplete evidence.
The reader output answers another question: what records and fields does the selected CSV dialect produce? A quoted field may legitimately contain a newline. Several physical lines can therefore belong to one record. Counting text lines or blindly splitting on newlines is not a complete CSV parser.
Correct the producer before filtering the result
If the unwanted translation is confirmed, change the file-opening boundary, regenerate a separate output and compare the parsed records. Keep the old file until you have checked the new one. Do not globally remove carriage returns or blank text lines: that can alter a field's legitimate data or hide a producer error.
Changing the writer's line terminator is a different control from disabling text-layer translation. Choose a dialect and terminator required by the receiving system, then test that combination. A workaround that looks good in one editor may still violate the consumer's expected CSV format.
If the reader returns the expected rows but a spreadsheet still displays gaps, retain that distinction. Investigate the application's import behavior and the actual fields rather than asserting that newline="" repairs every display issue.
Completion checks
- Record the Python version, open mode, encoding and newline setting.
- Inspect a small output's raw row-ending bytes.
- Parse the output with the intended CSV dialect.
- Compare record count and field values, including an embedded newline if supported.
- Import the regenerated copy into the real consumer and inspect the result.
If the records are correct but letters are garbled in Excel, the encoding and original-bytes guide addresses a different boundary. A correct row ending does not establish that the receiving application chose the right character encoding or date locale.
Checked November 8, 2026. Explicit text-wrapper translation and the logical-record round trip were tested locally with Python 3.9.6. No Windows host, Excel UI, original dataset or all-dialects compatibility test was performed.