Create a CSV list of files in a folder
Scan subfolders and record file paths, extensions, sizes, and modification times in a table. Start with 4 small example files while keeping the originals and existing results intact.
Split a CSV by data-record count, repeat its header in every output file, and leave the original unchanged. Use a seven-record synthetic example to check the boundaries and verify that all records survive in their original order.
Who this is forThis guide is for people who need to divide a CSV export into smaller files without editing its contents manually.
The script creates numbered CSV files containing at most a chosen number of data records. Each file starts with the same header. The header does not count toward the limit. With a limit of 3 and 7 data records, the files contain 3, 3, and 1 data records.
A CSV record is not necessarily a physical text line. A quoted field can contain a line break. The csv module reads records according to CSV quoting rules, so a multiline note stays attached to its record instead of becoming a separate row.
This dataset is synthetic and was written specifically for this article. Save it as sample.csv in your working folder. Copy the line break inside the quoted note exactly; it deliberately exercises a case that ordinary line splitting mishandles.
record_id,team,units,note
R001,Support,12,"starter, pack"
R002,Ops,7,repeat
R003,Support,9,"two
lines"
R004,Sales,5,normal
R005,Ops,11,normal
R006,Sales,6,normal
R007,Support,10,normal
There are 4 columns and 7 data records. The comma in starter, pack belongs inside one field. The note belonging to R003 spans 2 physical lines but remains one field. The units values sum to 60: 12 + 7 + 9 + 5 + 11 + 6 + 10.
python split_large_csv.pyRelative paths are resolved from the terminal's working directory, not automatically from the script's location. The parent outputs folder may already exist, but outputs/sample_parts must not exist when this run starts.
The splitter reads records incrementally and opens each part in exclusive creation mode. After writing, it rereads the source and parts to compare headers, field values, record order, and counts. Success messages appear only after that comparison finishes.
import csv
from pathlib import Path
SOURCE = Path("sample.csv")
OUTPUT_DIR = Path("outputs") / "sample_parts"
ROWS_PER_FILE = 3
def part_path(number: int) -> Path:
return OUTPUT_DIR / f"part_{number:04d}.csv"
def split_csv() -> tuple[int, int]:
if type(ROWS_PER_FILE) is not int or ROWS_PER_FILE < 1:
raise ValueError("ROWS_PER_FILE must be a positive integer.")
with SOURCE.open("r", encoding="utf-8-sig", newline="") as source:
reader = csv.reader(source, strict=True)
header = next(reader, None)
if not header or any(not name.strip() for name in header):
raise ValueError("Missing header or blank column name.")
if len(set(header)) != len(header):
raise ValueError("Duplicate column names.")
OUTPUT_DIR.parent.mkdir(parents=True, exist_ok=True)
OUTPUT_DIR.mkdir() # Refuse to reuse an existing output path.
total = 0
parts = 0
target = None
try:
for record_no, row in enumerate(reader, start=1):
if len(row) != len(header):
raise ValueError(
f"Data record {record_no}: wrong number of fields."
)
if total % ROWS_PER_FILE == 0:
if target is not None:
target.close()
parts += 1
target = part_path(parts).open(
"x", encoding="utf-8", newline=""
)
writer = csv.writer(target)
writer.writerow(header)
writer.writerow(row)
total += 1
finally:
if target is not None:
target.close()
return total, parts
def verify_csv(expected_rows: int, part_count: int) -> None:
with SOURCE.open("r", encoding="utf-8-sig", newline="") as source:
original = csv.reader(source, strict=True)
header = next(original, None)
seen = 0
for number in range(1, part_count + 1):
with part_path(number).open(
"r", encoding="utf-8", newline=""
) as part:
rows = csv.reader(part, strict=True)
if next(rows, None) != header:
raise ValueError(f"Header mismatch in part {number}.")
count = 0
for row in rows:
if row != next(original, None):
raise ValueError(f"Data mismatch in part {number}.")
count += 1
required = min(
ROWS_PER_FILE,
expected_rows - (number - 1) * ROWS_PER_FILE,
)
if count != required:
raise ValueError(f"Row count mismatch in part {number}.")
seen += count
if seen != expected_rows or next(original, None) is not None:
raise ValueError("Overall row count mismatch.")
if __name__ == "__main__":
total, parts = split_csv()
verify_csv(total, parts)
print(f"Verified {total} data records in {parts} files.")
print(f"Output folder: {OUTPUT_DIR.as_posix()}")
Input decoding accepts UTF-8 with or without a leading byte-order mark. Output uses UTF-8 without that mark. The newline argument lets the csv module handle record endings and embedded line breaks.
The expected files are listed below. Their units totals are manual cross-checks, not calculations performed by the splitter. Every file must repeat record_id,team,units,note as its header.
| File inside outputs/sample_parts | Record IDs | Data records | Units total |
|---|---|---|---|
| part_0001.csv | R001, R002, R003 | 3 | 28 |
| part_0002.csv | R004, R005, R006 | 3 | 22 |
| part_0003.csv | R007 | 1 | 10 |
The expected console text is shown below. It was derived by hand and is not a captured execution log.
Verified 7 data records in 3 files.
Output folder: outputs/sample_partsCheck that 28 + 22 + 10 equals the original total of 60. Open the first part in a text editor to inspect the quoted comma and multiline note. Counting visible lines alone will give the wrong data-record count.
Test in separate folders: 6 records produce two parts of 3; a header-only input produces no parts; a completely empty file is rejected. The boundary tests and execution results are recorded in the verification section below.
| Symptom | What to check |
|---|---|
| FileNotFoundError | Check SOURCE and the terminal's working directory. Confirm the filename is sample.csv rather than sample.csv.txt. |
| FileExistsError | Review the existing destination, then select a new folder beneath outputs. Even an empty existing destination is refused. |
| UnicodeDecodeError | Confirm the source encoding. Do not discard decoding errors; obtain or create a correctly decoded copy before splitting. |
| Missing or duplicate header | Provide nonblank column names. Exact duplicate names are rejected; header text is otherwise preserved. |
| Wrong number of fields or csv.Error | Inspect delimiters, quotes, and blank records. This script expects comma-separated input with standard double-quote quoting. |
| Failure during writing or verification | Treat that destination as incomplete. Review the error and use a new destination for a corrected run. |
A late error can leave partial files. The script closes its current file but does not roll back the folder or automatically delete anything.
After the example passes on your machine, change SOURCE, choose a fresh OUTPUT_DIR beneath outputs, and increase ROWS_PER_FILE. A record limit is not a byte-size limit: long fields can make equally sized groups occupy very different amounts of disk space.
The script preserves parsed field strings, not original bytes, quoting style, or record-ending format. It does not load the entire dataset, but unusually large fields still require memory and may exceed the CSV parser's field-size limit. Verification adds another read of the source and outputs. Keep the source unchanged throughout; this is not a locked snapshot or transactional backup.
ChatGPT Python tool · Python 3.13.5 · standard library csv/pathlib
The explanations and examples were written for this site. See the official sources below for the related behavior and concepts.