Skip to content
datahacks.dev
CSV

Choosing Between csvkit and Miller for Everyday CSV Work

csvkit and Miller timed on the same 200,000-row file. Type inference makes csvkit's sort, stat and JSON commands 100 times slower and can rewrite your dates.

6 min read
Summarize in AI
Flat illustration of a spreadsheet grid with a selected cell and a formula bar.

Both tools turn the command line into a decent CSV workbench, and the question of which to learn first comes up often. They are not close substitutes in how they behave. csvkit is a set of small Python programs with a friendly manner and a lot of conversion ability. Miller is a single compiled program with a record-oriented language and an eye on speed. I ran the same ten operations through both on a 200,000-row file and watched what they did to the data as well as how long they took. Some of the results were bigger than I expected, which is why the comparison is worth reading before you pick. The other tools in this space are surveyed in CSV tools worth knowing.

Ten operations, timed

Five pairs of equivalent commands, each tool's output discarded so that printing does not count:

OperationcsvkitMiller
Select two columnscsvcut -c order_id,amount: 0.57 smlr cut -f order_id,amount: 0.14 s
Filter rows by valuecsvgrep -c status -m failed: 0.57 smlr filter '$status == "failed"': 0.14 s
Sort by a number, descendingcsvsort -c amount -r: 57.85 smlr sort -nr amount: 0.47 s
Mean of a columncsvstat --mean -c amount: 57.48 smlr stats1 -a mean -f amount: 0.14 s
Convert to JSONcsvjson: 58.88 smlr --c2j cat: 0.39 s

Two groups. For the first pair, the commands that just pass rows through, csvkit is about four times slower than Miller, which is what you expect from Python against a compiled tool and does not matter at this size. For the rest, csvkit is 120 to 400 times slower, nearly a minute for a 12 MB file. Something specific is going on.

Why a minute

csvkit's commands that need to know a column's type, to sort numerically, to compute a statistic or to write JSON with numbers as numbers, read the entire file first to infer the type of every column, and only then do the work. That scan is slow in Python, because it tries several parsers on each value, including dates and times. csvcut and csvgrep need no types, so they skip it.

The inference can be switched off with -I (--no-inference), and the timing changes completely:

Text
csvjson -I                  2.66 s
csvsort -I -c amount -r     2.28 s
csvstat -I --count          0.52 s

From 58 seconds to 2.3 seconds for the sort. The price is in the output. With no inference, every column is text, and the sort is a text sort. The top rows of csvsort -I -c amount -r began with 99.99, and the largest amount in the file is 651.04:

Text
$ csvsort -I -c amount -r orders-200k.csv | head -3 | cut -c1-60
order_id,customer_id,zip,amount,created_at,status,country,no
7422,216899,81330,99.99,2025-06-23 17:36:38,failed,JP,rush o
16697,46867,54251,99.99,2025-04-05 11:19:19,paid,JP,

A string "99.99" sorts after "651.04" in reverse, because 9 is greater than 6. This is the failure you are warned about when you drop type inference, and it is silent. The fast flag is fine for csvstat --count or csvjson where you want the text, and wrong for sorting numbers. If you need both speed and a numeric sort, that is the point at which Miller, with its numeric sort flags, or a SQL tool, does the job without the dilemma.

Inference changes values

There is one more consequence, and it is the one I would worry about most. With inference on, csvsort does not only read the types, it writes values using them. The input timestamp was 2025-04-28 21:12:46. In the output of the typed csvsort, the same field read:

Text
$ csvsort -c amount -r orders-200k.csv | grep -m1 '^158192,' | cut -d, -f1,5
158192,2025-04-28T21:12:46

The space became a T: csvkit parsed the string as a datetime and wrote it back in ISO 8601 form. Here that is benign, even helpful, and in general it means the output of a type-inferring command is not byte-for-byte the input. Numbers lose trailing zeros, and a date in an odd format may be rewritten or, if the sniffer is confused, read as text. csvcut, which does no inference, and Miller, which passes values through unchanged unless you modify them, both returned 2025-04-28 21:12:46 for the same row. If your pipeline compares output to input, or writes back to a system with a strict format, this matters. It does not mean csvkit is wrong; it means every conversion in the chain deserves a diff of a sample.

What each is good at

Miller's strengths are speed and range. It reads and writes CSV, TSV, JSON, JSON Lines, a name-index format, a pretty-print format and markdown, converts between them with one flag, has verbs for most record operations and an expression language for computing new fields. A single binary installs anywhere. It handles quoting correctly, and its numbers are floating point, so sums carry binary noise (3196886.2000000123) until you format them (Miller for records and pipelines).

csvkit's strengths are the odd corners. in2csv converts Excel workbooks (both .xls and .xlsx), fixed-width files given a schema, JSON and other formats to CSV, which makes it the quickest way to get an Excel sheet out (converting Excel to CSV compares it with others). csvsql runs SQL on a CSV through an in-memory SQLite and can create tables and insert into PostgreSQL or MySQL. csvstack and csvjoin combine files, csvlook renders a readable table, csvformat changes dialects, and csvclean validates (CSVLint and other validators). The documentation is excellent, and the commands are named like the Unix tools they echo, so the learning curve is low (getting started with csvkit).

csvkitMiller
Installpip install csvkitOne binary
Speed, no type needsModerateFast
Speed, with typesVery slow without -IFast
Output equals inputNot when inferringYes, unless you modify the field
Excel and fixed-width inputin2csvNot directly
Load into a databasecsvsql --insertNot directly
New computed columnsVia csvsqlput expressions
FormatsCSV, TSV, JSON, fixedCSV, TSV, JSON, JSONL, XTAB, markdown, more

A rule I would use

If the job runs in a script or touches a file over a few megabytes, use Miller. If the job is converting an Excel file, loading a table into a database or exploring a small file with a gentle interface, use csvkit. If you find yourself adding -I to csvkit to make it fast, you have chosen the wrong tool for that job, since the flag removes the thing csvkit's typed commands exist to do.

A second rule applies to both. Whatever tool you use, read identifiers as text, and diff a sample of the output against the input. The checks that apply are the same as in validating a CSV file, and the typed-versus-text trap is the subject of why a CSV file has no types and what that costs you.

When csvkit is the better pick

Pick csvkit when the file is small and the task is one-off, when you need the converters, when a colleague already knows it or when you want the SQL interface without installing a database. At 200,000 rows, a one-minute wait on a rare command is acceptable. Pick it too when its documentation and error messages matter more than speed, as when teaching, since csvkit's commands read like a tutorial.

Pick neither when the file is large and the question is analytic: DuckDB read the 2 million row version of this file and grouped it in 0.20 seconds (querying a CSV file with DuckDB instead of loading it). And do not pick on speed alone. The documentation for each is the reference for flags: csvkit and Miller.

Also inTooling
Share

Keep reading

Get new articles by email

We send one email when a new article is published. No spam.

Unsubscribe in one click. No spam, ever.