Skip to content
CSVTidy

How to Split a Large CSV File Into Smaller Ones

3 min read

Split a big CSV by row count or by column value using split, Python, or a browser tool — and keep the header row on every part.

You need to split a CSV because something downstream has a limit: an import capped at 10,000 rows, an email tool that rejects files over 5 MB, or Excel refusing to open more than 1,048,576 rows.

Splitting is easy. Splitting correctly means every output file is still a valid CSV — which mostly comes down to the header row.

The command line: fast, with one flaw

split is built into macOS and Linux and handles enormous files effortlessly:

split -l 10000 big.csv part_

That produces part_aa, part_ab, and so on, at 10,000 lines each.

The flaw: only the first file has the header row. Every other part starts mid-data, so importing them fails or treats a real record as the header.

Fix it by re-attaching the header afterwards:

# Save the header, split the body, then prepend the header to each part
head -n 1 big.csv > header.txt
tail -n +2 big.csv | split -l 10000 - part_
for f in part_*; do
  cat header.txt "$f" > "$f.csv" && rm "$f"
done
rm header.txt

Add --additional-suffix=.csv on GNU split to skip the rename step.

The second flaw is more serious: split works on lines, not CSV records. A quoted field containing a newline —

id,notes
1,"line one
line two"

— is one record across two lines. split may cut it in half, corrupting both files. If your data contains multi-line fields, use a real CSV parser instead.

Python: correct for any CSV

import csv

ROWS_PER_FILE = 10000

with open("big.csv", newline="", encoding="utf-8-sig") as f:
    reader = csv.reader(f)
    header = next(reader)

    part, count, writer, out = 1, 0, None, None
    for row in reader:
        if count % ROWS_PER_FILE == 0:
            if out:
                out.close()
            out = open(f"part_{part:03d}.csv", "w", newline="", encoding="utf-8")
            writer = csv.writer(out)
            writer.writerow(header)
            part += 1
        writer.writerow(row)
        count += 1
    if out:
        out.close()

This streams — memory use stays flat regardless of file size, so it handles a 10 GB file on a laptop. Note {part:03d}, which zero-pads the numbers so part_002 sorts before part_010. Without padding, any tool processing them alphabetically handles your files in the wrong order.

Splitting by column value

Often more useful than fixed sizes: one file per region, per department, per month.

import csv
from collections import defaultdict

groups = defaultdict(list)
with open("sales.csv", newline="", encoding="utf-8-sig") as f:
    reader = csv.DictReader(f)
    for row in reader:
        groups[row["region"]].append(row)
    header = reader.fieldnames

for value, rows in groups.items():
    safe = "".join(c if c.isalnum() or c in "-_" else "_" for c in value)
    with open(f"{safe or 'blank'}.csv", "w", newline="", encoding="utf-8") as f:
        w = csv.DictWriter(f, fieldnames=header)
        w.writeheader()
        w.writerows(rows)

Sanitizing the value into a filename matters — a region called North/South would otherwise try to write into a directory that doesn't exist, and an empty value produces a file named .csv that your file manager hides.

Picking a split size

Match the limit you are actually working around:

| Constraint | Practical size | |---|---| | Excel row limit | under 1,000,000 rows | | Most CRM imports | 5,000–10,000 rows | | Email attachment | check MB, not rows | | API batch endpoints | whatever the docs say, usually 500–1,000 |

For a size limit rather than a row limit, estimate from the whole file: divide total bytes by row count to get average row size, then divide your target size by that. Round down generously — one oversized part means redoing the whole split.

Verify before you delete the original

Row counts should reconcile exactly:

# Sum of all parts, minus one header per part, should equal the original body
wc -l part_*.csv
wc -l big.csv

If you produced 12 parts, the total line count should be the original plus 11 — the extra header rows.

Keep the original until the downstream import has actually succeeded. Splitting is cheap to redo from the source and impossible to undo from the parts if one went missing.

Checklist

  • Every part needs the header row.
  • Zero-pad part numbers so they sort correctly.
  • Use a CSV parser, not line splitting, if any field may contain newlines.
  • Sanitize values used as filenames.
  • Reconcile row counts before deleting the original.

Keep reading