๐ CSV Fundamentals ยท 7 min read
CSV Writer Adding Blank Rows: Why csv.writer Puts an Empty Line Between Records
csv.writer is not adding those blank rows โ the text layer is. The writer terminates every record with \r\n itself, and if you opened the output file without newline='', Python's text layer translates the \n inside that pair into another \r\n, so the file ends up holding \r\r\n. Parsers read the doubled break as an empty record. The fix is one argument: open(path, "w", newline="", encoding="utf-8").
This page is the write side of the problem, so it starts before the file exists. If you were handed a file that already has empty rows and you just want them gone, removing blank rows from a CSV is the cleanup page and it does not care why they are there. If you are on the reading end instead โ csv.reader, pandas.read_csv, delimiters and encodings โ opening a CSV in Python covers that. What follows is why the blank lines get written in the first place, and the three other ways the same symptom shows up.
Why does csv.writer add an extra blank line?
Two independent layers are being polite about line endings at the same time. The first one is the csv module. Its writer has a setting called lineterminator, and the documentation defines it in one sentence:
"The string used to terminate lines produced by the writer. It defaults to
\r\n."
So the writer has already produced a complete CRLF record ending. It does not need any help, and that is the whole point of the setting: RFC 4180 asks for CRLF, and Python writes CRLF whatever platform you are on. The RFC 4180 rules are short and this is one of them.
The second layer is open(). When you write text, it decides what to do with every \n that passes through:
"When writing output to the stream, if newline is None, any
\ncharacters written are translated to the system default line separator,os.linesep. If newline is''or\n, no translation takes place."
Leave the argument out and it is None, which means translate. On Windows os.linesep is \r\n, so the \nhalf of the writer's \r\n becomes \r\n a second time. The two bytes become three: \r\r\n.
Python's documentation says this outright, in a footnote attached to the writer, which is the part almost nobody reads:
"If csvfile is a file object, it should be opened with
newline=''... Ifnewline=''is not specified, newlines embedded inside quoted fields will not be interpreted correctly, and on platforms that use\r\nline endings on write an extra\rwill be added."
That is not a warning about a rare edge case. It is a description of what happens on every Windows machine, every time, and it also names the second failure nobody notices until a field contains a line break: a multi-line quoted value gets mangled the same way.
What the file looks like, byte for byte
The symptom is easy to recognise once you know to look for a stray \r at the end of each line. In a text editor it looks like nothing at all; in a hex view or a repr() it is obvious.
Measured on this machine, 2026-11-01, Python 3.13.5 on Linux, four records (header plus three data rows of three columns):
| How the file was opened | Bytes | Doubled breaks | Rows a parser sees |
|---|---|---|---|
newline='' | 49 | 0 | 4 |
newline left at None, translating stream | 53 | 4 | 8 (four of them empty) |
Four extra bytes, four extra records. The count matches exactly because the damage is one byte and one record per line โ a 100,000-row export gains 100,000 empty rows and 100,000 bytes, which is why the file also looks "about the same size" and nobody suspects a line-ending problem.
What is the fix?
Tell the text layer to stop translating. The csv module already handles newlines, so switching that handling off is safe on every platform and is what the documentation recommends:
import csv
with open("out.csv", "w", newline="", encoding="utf-8") as f:
writer = csv.writer(f)
writer.writerows(rows)Two details in that line are worth keeping. newline="" is notnewline="\n" โ the empty string disables translation entirely, while the named values change what \n becomes. And encoding="utf-8" is there because the default is the locale encoding, which is how the same script produces cp1252 files on one machine and UTF-8 files on the next.
The read side takes the same argument, for the same reason: open(path, "r", newline="", encoding="utf-8") hands the raw line endings to csv.reader instead of normalising them first.
To confirm the fix, read the file back and count records instead of trusting the spreadsheet view:
with open("out.csv", "rb") as f:
raw = f.read()
print(len(raw), raw.count(b"\r\r\n")) # 49 0Does it happen on Linux too?
Not in that form. The translation is to os.linesep, and on Linux and macOS that is \n, so the writer's \r\n passes through unchanged and the file is correct by accident. That is exactly what makes this bug expensive: the same code is fine on your laptop and broken on the machine that runs the nightly export.
Leaving out newline='' is still wrong on Linux, only quieter. A quoted field that contains a line break gets its interior newlines translated too, so a notes column with a paragraph in it arrives with different bytes depending on the platform. The csv module cannot parse that reliably, which is the first half of the documented footnote.
You can also meet the result without writing it: a file produced on Windows and read on Linux keeps its \r\r\n sequences, because reading only translates when you ask it to. At that point you are the consumer, and stripping the empty rows is a reasonable one-off โ as long as the writer gets fixed, or the file will come back next week.
What about pandas.to_csv?
pandas has the same exposure, and the deciding factor is who opens the file. Give to_csv() a path and it manages the stream itself:
df.to_csv("out.csv", index=False) # 45 bytes, no doubled breaks
with open("out.csv", "w") as f: # you opened it: translation is back on
df.to_csv(f, index=False)In our measurement the path version wrote 45 bytes with zero doubled breaks. The same dataframe written as \r\n into a stream that translates newlines produced 53 bytes, four \r\r\n sequences and eight parsed records for four rows of data. Nothing about pandas changed between those two lines except the second argument.
If you do need the file object โ for a buffer, an upload, or a streamed response โ open it with newline='' and let pandas write its own terminators. If you suspect a pandas file already has the problem, the field-count and row-count checks in setting sep in pandas will show it as a row count that does not match the data.
Does append mode make it worse?
Append mode does not create the doubled break, but it multiplies it. A loop that appends one record per iteration repeats the same mistake on every pass, and because each run adds to a file that already exists, the defect accumulates instead of being overwritten.
Measured: two appends of one data row each, through a translating stream, wrote 25 bytes and a parser read four records โ two real rows and two empty ones. Opened with newline='', the same two appends write two clean records.
There is a second, unrelated append trap worth knowing. Append mode does not check that the file ends with a terminator. If a previous run died mid-write, the last record has no line ending, and the next append glues its first field onto it, producing one merged record rather than a blank one. Both look like "the file is corrupted" from a spreadsheet, and they need different repairs.
Is a file with \n instead of \r\n a problem?
Sometimes, and the fix is not to remove newline=''. RFC 4180 section 2 is explicit about the expected terminator: "Each record is located on a separate line, delimited by a line break (CRLF)." A consumer that implements the spec strictly is entitled to see \n-only files as one long line.
Most consumers are lenient. Python's reader documents that it "is hard-coded to recognise either \r or \n as end-of-line, and ignores lineterminator", and our Linux-written file with plain \n endings parsed as four correct records. Excel on Windows and a few older importers are the ones that care.
When a receiver demands CRLF, set it on the writer and leave the file object alone:
with open("out.csv", "w", newline="", encoding="utf-8") as f:
csv.writer(f, lineterminator="\r\n").writerows(rows)That gives exactly one CRLF per record. Setting lineterminator="\r\n" on a file opened without newline='' gives the doubled break again โ the two settings have to agree, and the file object is the one that wins.
Which cause do you have?
Four symptoms, four different repairs. Read the bytes before you change code.
| Symptom | Cause | Fix |
|---|---|---|
Empty record after every row, \r\r\n in the file | Output opened without newline='' | Add newline='' to open() |
| Empty rows pile up across runs | Same omission inside an 'a' loop | Fix the writer, then rewrite the file once |
| One merged record where two belong | Previous run left no final terminator | Repair the last line before appending |
| Importer rejects the file, no blank rows | Consumer expects CRLF, got \n | Set lineterminator="\r\n", keep newline='' |
One habit covers all four: after writing, print the byte count and the count of b"\r\r\n". A single assertion in the script is cheaper than discovering the problem in a downstream import next month, and it is the same check that catches a stray quote or a truncated final record before the file leaves your machine.
Frequently Asked Questions
Why does csv.writer add an extra blank line between records?
Because two layers both write a line ending. csv.writer terminates every record itself, and its default lineterminator is \r\n. If you open the output file without newline='', Python's text layer translates any \n it receives to the platform line separator, so each \r\n becomes \r\r\n on Windows. A parser reads the extra break as the end of an empty record, which is the blank line you see.
What is the correct way to open a file for csv.writer?
Open it with newline='' and an explicit encoding: open(path, 'w', newline='', encoding='utf-8'). Python's csv documentation says that if csvfile is a file object it should be opened with newline='', and warns that otherwise newlines inside quoted fields are not interpreted correctly and an extra \r is added on platforms that use \r\n line endings on write. The csv module does its own newline handling, so the text layer must be told to stop translating.
Does the blank-line bug happen on Linux and macOS?
Not in the same form. Leaving newline at its default translates \n to os.linesep, and on Linux and macOS os.linesep is \n, so \r\n stays \r\n and the file is correct by accident. The bug appears as soon as the same code writes on Windows, in a Windows container, or in a job you hand to a colleague on Windows. newline='' is still the documented setting on every platform, because it also protects the line breaks inside quoted fields.
Does pandas.to_csv create blank rows too?
It can, through the same mechanism, but only when you hand it a file object you opened yourself. Passing a path lets pandas manage the stream. In our test on 2026-11-01, to_csv(path, index=False) wrote 45 bytes with no doubled breaks, while to_csv() writing \r\n into a stream that translates newlines produced 53 bytes, four doubled breaks and eight parsed records for four rows of data.
Does append mode make the blank rows worse?
It compounds them. Append mode adds to a file that already exists, so a mistake made inside the loop is repeated on every run instead of once. In our test, two appends through a translating stream wrote 25 bytes for two data rows and a parser read four records, half of them empty. Opening the file in append mode with newline='' removes the cause at its source; deleting the blank rows afterwards only hides it.
Is a file with \n instead of \r\n a problem?
RFC 4180 section 2 says each record is located on a separate line, delimited by a line break (CRLF), so a strict reader is entitled to expect \r\n. In practice Python's reader is hard-coded to recognise either \r or \n as end-of-line and ignores lineterminator, and most modern parsers do the same. The real risk is older Windows software and some importers. If a consumer insists on CRLF, set lineterminator='\r\n' on the writer and keep newline='' rather than removing it.
Sources
- Python 3 documentation, csv โ CSV File Reading and Writing (retrieved 2026-11-01):
csv.writerโ "If csvfile is a file object, it should be opened withnewline=''"; footnote 1 โ "Ifnewline=''is not specified, newlines embedded inside quoted fields will not be interpreted correctly, and on platforms that use\r\nline endings on write an extra\rwill be added. It should always be safe to specifynewline='', since the csv module does its own (universal) newline handling";Dialect.lineterminatorโ "The string used to terminate lines produced by the writer. It defaults to\r\n" and "The reader is hard-coded to recognise either\ror\nas end-of-line, and ignores lineterminator." - Python 3 documentation, Built-in Functions โ open()(retrieved 2026-11-01): "When writing output to the stream, if newline is None, any
\ncharacters written are translated to the system default line separator,os.linesep. If newline is''or\n, no translation takes place. If newline is any of the other legal values, any\ncharacters written are translated to the given string." - RFC 4180, Common Format and MIME Type for CSV Files, section 2 (retrieved 2026-11-01): "Each record is located on a separate line, delimited by a line break (CRLF)"; "Fields containing line breaks (CRLF), double quotes, and commas should be enclosed in double-quotes."
- Local measurement, 2026-11-01, Python 3.13.5 and pandas 2.3.3 on Linux, script and raw output kept in
/tmp/nc_m/: four records of three columns written withnewline=''gave 49 bytes, 0\r\r\nand 4 parsed rows; the same records written through a stream translating\nto\r\n(the Windows text-layer behaviour, reproduced withnewline="\r\n"on Linux) gave 53 bytes, 4\r\r\nand 8 parsed rows;DataFrame.to_csv(path, index=False)gave 45 bytes and 0 doubled breaks, whileto_csv()into a translating file object gave 53 bytes, 4 doubled breaks and 8 rows; two single-row appends through a translating stream gave 25 bytes and 4 parsed records.
Clean Up a File You Already Have
Fixing the writer stops new blank rows. For the export that already contains them, the cleaner removes empty records and then counts rows before and after so you can see what changed. Nothing is uploaded.
Related reading
File Operations โ other guides that pair well with this one.
- CSV Delimiter in pandas: How to Set sep (and What Happens When You Don't)
- Excel CSV Separator Setting: Why a Comma-Delimited File Has No Commas
- Convert CSV to Excel Without Excel
- Convert Excel to CSV Free Online
Browse all guides in the NoCodeCSV blog.