CSVFormat CSV Converter

CSV Dialects: RFC 4180 vs Excel vs Pandas vs DuckDB

CSVFormat — CSV Formatter & Converter Guides · Updated 2026-10-12 · All guides

"CSV" is not a format, it is a family. RFC 4180 defines about five rules; every producer in the table below adds or relaxes a few. The useful model is that a CSV file has dialect settings, and almost every parse failure is a dialect mismatch rather than a malformed file.

The five settings that differ

SettingValues seen in the wildDefault that breaks
Delimiter, ; \t |comma, on a European export that uses ;
Quote char" '" when the file uses ' — quotes silently become data
Escape style"" doubling · \ backslashdoubling, on a Postgres text-mode dump that uses \N and backslashes
NULL marker\N · empty · NULL · NAempty string, so "no value" and "empty value" merge
Line ending\n · \r\nreader splits on \n, \r sticks to the last column

Plus three that libraries add: whether the first row is a header, whether a trailing newline is required, and whether whitespace outside quotes is significant.

What each tool writes, and what it chokes on

ToolWritesReads by defaultNotable
RFC 4180CRLF, " doubling, quote only when required—the spec allows a quoted newline; it does not define \ escapes
Excellocale delimiter, CRLF, quotes when required; BOM with "CSV UTF-8"guesses; offers a delimiter pickerwill not re-read what it wrote if the system list-separator changed since
Python csv\r\n, " doubling" doubling, \n splitlineterminator default is \r\n for the writer, \n for the reader — asymmetric by design
pandas\n by defaultsniffs delimiter from a sample (engine='python' sniffs; c does not)read_csv guesses types too, which is a separate and worse default
DuckDB read_csv\n, " doublingauto_detect=true samples the fileinference is right often enough to hide the failure until a 01234 column arrives
Postgres COPY … CSV\n server-side, \N for NULL"" doublingNULL handling differs from both Excel and pandas
MySQL INTO OUTFILEtab-delimited, \n, \N, no quoting by default—the most surprising one: a .csv from MySQL is often a TSV wearing a hat
Go encoding/csv\n (not CRLF, spec-noncompliant)\n and \r\n acceptedFieldsPerRecord errors loudly where most readers pad

Two asymmetries to memorise: Python writes CRLF and reads LF; Go writes LF where the spec asks for CRLF. Both are documented, both surprise someone every quarter.

The NULL problem, stated once

ProducerEmpty field meansNULL is written as
Excelempty stringindistinguishable from empty
Postgres COPY CSVempty string\N (unquoted)
pandasNaNempty string by default (na_rep='')
MySQL LOAD DATAempty string\N
BigQuery exportempty stringempty string

So a Postgres→Excel→Postgres round-trip turns every NULL into an empty string, and every empty string into… whichever the receiving loader decides. Use NULL '' on one side and NULL 'NULL' on the other deliberately, per migration, and put the choice in the migration file rather than in someone's memory.

Sniffing a dialect

import csv
sample = open('mystery.csv', encoding='utf-8-sig', newline='').read(8192)
d = csv.Sniffer().sniff(sample)          # -> delimiter=';', quotechar='"'
print(d)

The sniffer is a heuristic over a 8 KB sample and will happily report a wrong delimiter for a file where the first 8 KB happen to be well-behaved. Prefer the shape test that never lies:

head -1 mystery.csv                                   # look at it
awk -F';' 'NR==1{print "semicolons:", NF}' mystery.csv
awk -F,   'NR==1{print "commas:",     NF}' mystery.csv

Whichever gives a plausible column count is the delimiter. A ;-delimited file parsed as comma-separated yields exactly one column and no error — the single most common silent failure in this format, and the reason the converter reports a row count you can sanity-check.

When to stop using CSV

Three honest triggers:

  • The file has a schema you care about. Then it wants Parquet or a typed format; CSV's type-free-ness is the bug you keep re-paying.
  • **Fields legitimately contain newlines and humans read the raw file.** Then the file is unreadable-by-humans already; use TSV (tabs in text are rarer than commas) or JSON Lines.
  • Two producers append to one file. Then the dialect drifts per section, and no parser setting fixes a file that is two dialects.

For a tool that must accept whatever arrives, the working pattern is: detect dialect, parse to a typed intermediate, then emit a canonical dialect — never re-emit the input dialect, because that is how the drift continues. See data cleaning for the order of operations and encoding and the BOM for the step that comes before all of this.

Developer Sponsor / Partner
Copied to clipboard!