CSV#
Comma-Separated Values. The simplest tabular format that anyone can read, write, and exchange between systems that disagree on everything else. Predates the internet; survives to outlive most of its replacements.
CSV is also the format people most often almost parse correctly.
The Format#
The intent: rows separated by newlines, fields separated by
commas. The reality is messier because field values may
contain commas, newlines, and quotes; that is why “just
split(',')” gets the wrong answer for any non-trivial
input. RFC 4180 codified a common interpretation in 2005.
id,name,email
1,Ada,operator@example.com
2,Alan,alan@example.com
3,Grace,grace@example.com
The reality is messier because.
Field values may contain commas.
Field values may contain newlines.
Field values may contain quotes.
Different tools use different conventions.
RFC 4180 codified a common interpretation in 2005, but older / national / vendor variants still exist.
RFC 4180 Rules#
RFC 4180 is the closest thing to a standard CSV
specification. The rules below cover separator, line ending,
header, and (crucially) the quoting and escaping rules
that distinguish a real CSV parser from split(','):
Fields separated by commas.
Records separated by
CRLF(\r\n).Each record has the same number of fields.
The first record may be a header.
Fields may be enclosed in double quotes.
If a field contains a comma, newline, or quote, it must be quoted.
Quotes inside a quoted field are escaped by doubling:
"".
id,name,note
1,Ada,"Hello, World"
2,Alan,"Line 1
Line 2"
3,Grace,"She said ""hi"""
The Dialects#
CSV is really a family of related formats. The variations below come from regional locale differences, vendor history, and pragmatic adaptations. The format itself doesn’t self-describe, so the receiver has to be told which dialect to expect.
Dialect |
Notes |
|---|---|
RFC 4180 |
|
Excel (US locale) |
same; sometimes BOM at start |
Excel (European locale) |
|
Unix CSV |
LF line endings; otherwise RFC 4180 |
Unquoted minimal |
no quoting; breaks on any embedded delimiter |
Pipe-separated (PSV) |
|
Tab-separated (TSV) |
|
The lack of self-description means the receiver has to be told the
dialect. Tools that auto-detect (csvkit’s csvclean, Python
csv.Sniffer) help but aren’t reliable.
Parsing Pitfalls#
The bugs every CSV pipeline runs into. The single most common is treating CSV as a “split on comma” format, which fails the moment a value contains a comma, a newline, or a quote. Embedded delimiters, locale differences, BOMs, and quoting inconsistencies are the rest of the standard landmines.
id,description
1,The price is $3,000
Naïve parsers split on every comma; this row has 3 fields, not 2. The correct CSV would be.
id,description
1,"The price is $3,000"
Other classic traps.
Embedded newlines, only handled correctly inside quoted fields. Splitting on
\nfirst then on,breaks immediately.Trailing whitespace, `` Ada `` and
Adaare different values; some readers trim, some don’t.Empty vs. missing,
a,,bhas three fields where the middle is empty. Whether “empty” meansnull,"", or zero is a schema decision your CSV doesn’t capture.Quoted numbers vs. strings,
"123"is text in some parsers, a number in others.Locale-dependent decimals,
3,14is “3.14” in German / French CSVs; an unquoted comma will misalign columns.BOM at start, Excel writes a UTF-8 BOM (
); the first column header becomesidif not stripped.Mixed line endings, some rows
\r\n, some\n; some parsers tolerate, some don’t.Excessive escaping,
\"(backslash) instead of""(RFC-correct).
The lesson: use a real CSV parser. Don’t split(",") outside a
disposable shell one-liner.
Common Encodings#
CSV files arrive in many encodings. UTF-8 is the modern
default, but legacy systems still produce UTF-16, Windows-
1252, and regional Asian encodings. The standard operator
move is “convert to UTF-8 at the boundary” with iconv:
UTF-8 (with or without BOM), the modern default.
UTF-16, older Excel exports on Windows.
Windows-1252 / ISO-8859-1, many legacy systems.
Shift-JIS / EUC-JP / GBK, regional legacies.
Convert to UTF-8 at the boundary.
$ iconv -f WINDOWS-1252 -t UTF-8 input.csv > clean.csv
Detect encoding when you don’t know.
$ file -i input.csv
$ uchardet input.csv
CLI Tools#
The CLI tools that turn CSV from “load it into Excel” into
something operators can pipeline. csvkit is the Python-
based standard; xsv / qsv are the fast Rust
alternatives; Miller is the Swiss Army knife; DuckDB lets
you SQL-query CSV files in place.
csvkit,
csvcut,csvgrep,csvjoin,csvstat,in2csv,csvjson.xsv, Rust; very fast.
qsv, xsv fork with more commands.
mlr (Miller), “awk for CSV / TSV / JSON”; the Swiss Army knife.
csvlens, TUI viewer.
visidata, interactive TUI for exploring CSV-shaped data.
DuckDB,
SELECT ... FROM 'data.csv'reads CSV directly with full SQL.awk/sed, only for files where the format is simple enough to trust.
Per-Language Bindings#
The standard CSV libraries by language. Most languages ship something in their stdlib; the third-party options in the table are the ones that handle dialects, encoding, and streaming better than the stdlib defaults.
Language |
Library |
|---|---|
Python |
|
Go |
|
Rust |
|
JavaScript / TypeScript |
|
Java |
|
C# / .NET |
|
Ruby |
|
PHP |
|
For very large files (multi-GB), use streaming parsers (pyarrow,
polars lazy frames, xsv, qsv) rather than loading
everything into memory.
DuckDB Tricks#
DuckDB makes CSV practical for ad-hoc analytics by exposing SQL directly on top of CSV files, no load step, no schema declaration, full SQL semantics over pipe-separated, semi- colon-separated, multi-file globs. For analytical workloads, the standard pattern is “convert CSV to Parquet once and query that”:
-- Auto-detect everything
SELECT * FROM 'data.csv' LIMIT 10;
-- Force the dialect
SELECT * FROM read_csv('data.csv',
delim='|',
header=true,
types={'amount': 'DECIMAL(12,2)'});
-- Multi-file
SELECT * FROM 'data/*.csv';
-- Stream into Parquet
COPY (SELECT * FROM 'huge.csv')
TO 'huge.parquet' (FORMAT PARQUET, COMPRESSION 'zstd');
For analytical workloads, convert CSV to Parquet once and query that.
Where CSV Wins#
The reasons CSV outlives every “modern replacement” proposal. Universal compatibility, human readability, spreadsheet export, line-oriented streaming, and Git- friendly diffing are why CSV is still the default for public data publication and inter-system exchange.
Universal compatibility, every tool, every language, every era.
Human readable, you can
lessit.Easy export from spreadsheets, non-technical collaborators can produce it.
Streaming, line-oriented; works with pipes.
Diff-friendly, text; Git tracks changes line by line.
Public-data exchange, governments, statistics offices, finance filings.
Where CSV Loses#
The cases where another format would serve better. CSV’s lack of types, nesting, schema evolution, and binary efficiency become painful as data grows; analytical workloads at any non-trivial size belong in Parquet, not CSV.
Type information, everything is text; numbers, dates, booleans need conventions or out-of-band schema.
Nested data, arrays / objects don’t fit naturally.
Size, much larger than columnar / binary alternatives.
Performance, 5-50× slower than Parquet for analytics.
Schema evolution, header drift across files is painful.
Quoting overhead, escaping rules trip up many integrations.
When to Use CSV#
The use cases CSV is genuinely best at. Most boil down to “someone non-technical needs to look at this” or “we want a text format anything can ingest.” For long-term tabular storage at scale, switch to Parquet.
Data export for non-technical users (spreadsheet-bound).
Inter-system exchange with tools you don’t control.
Public data publication, universal access.
Small / medium ad-hoc data, < a few GB.
Logs / append-only writes, when you need streaming.
For long-term storage of tabular data at scale, use Parquet. For wire / API formats, use JSON.
A Stricter Alternative: TSV#
If you control both ends of the pipe, tab-separated is
usually a better choice; tabs almost never appear in real
data, so quoting is rarely needed and parsers can be
split('\t') simple again. See TSV for the full
picture.
Pitfalls Recap#
The condensed checklist. If a CSV pipeline is failing, walk this list; it covers the bulk of what goes wrong with parsing, dialect, encoding, BOMs, schema, and downstream analytics.
Don’t
split(','), use a real parser.Settle on a dialect; communicate it.
Specify the encoding; convert to UTF-8 if you can.
Strip BOMs.
Treat the schema as out-of-band (a separate doc, JSON Schema, or named columns).
For analytics, convert to Parquet once.
Workflow#
Extract, parse, filter, save. csvkit, mlr (Miller), or
awk on the command line; pandas or polars for anything
larger than memory.
Extract and inspect.
$ csvstat users.csv # per-column stats
$ csvcut -n users.csv # list columns with indexes
$ mlr --csv head -n 5 users.csv
Parse and filter.
# rows where status == active
$ csvgrep -c status -m active users.csv
# project columns
$ csvcut -c name,email,status users.csv
# Miller: predicate + column projection
$ mlr --csv filter '$age > 30' then cut -f name,email users.csv
# awk fallback (assumes no embedded commas in fields)
$ awk -F, 'NR==1 || $3=="active"' users.csv
Save.
$ csvgrep -c status -m active users.csv > active.csv
$ mlr --csv filter '$age > 30' then cut -f name,email users.csv > over30.csv
Python with pandas for non-trivial work.
import pandas as pd
df = pd.read_csv("users.csv")
active = df[df["status"] == "active"][["name", "email", "status"]]
active.to_csv("active.csv", index=False)