Semantic CSV↔CSV diff (schema + rows + cells) with zero runtime dependencies.
Project description
csvcomp
Semantic CSV↔CSV diff (schema + rows + cells) with zero runtime dependencies.
csvcomp compares two CSV/TSV files and reports:
- Schema changes — columns added, removed, or (optionally) renamed.
- Row changes — rows inserted, deleted, or modified. Rows can be matched positionally or by a key column (or composite key).
- Cell changes — for each modified row, exactly which cells changed.
Everything is zero runtime deps — dependencies = [] in pyproject.toml. The
whole library runs on Python 3.11+ stdlib only (csv, io, datetime,
decimal, collections, difflib, json, argparse).
Why this exists
Existing CSV-diff tools either refuse the most common production case, or aren't embeddable in Python:
pandas.DataFrame.compare()raisesValueErroron the first shape mismatch — a single added column in a new export breaks the diff. (Quoted verbatim from the pandas docs.)paulfitz/daffis a Java library with a Node CLI wrapper; no embeddable Python API.csvkitships 13 subcommands; none of them is a diff.yourlabs/csv-difftargets JSON-line key-based diffs, not CSV↔CSV.
csvcomp fills the gap: a single-file-library pure-Python diff that works
in CI, scripts, and pipelines without dragging in pandas, numpy, or any
third-party CSV parser.
Install
The package is not yet on PyPI. From a clean clone:
pip install git+https://github.com/prasad-a-abhishek/csvcomp.git
For local development:
git clone https://github.com/prasad-a-abhishek/csvcomp.git
cd csvcomp
pip install -e ".[test]"
pytest
CLI usage
csvcomp a.csv b.csv --key order_id --coerce amount:number,shipped_at:date --format unified
@@ columns @@
order_id (kept)
amount (kept)
shipped_at (kept)
+ tracking_id (added)
@@ rows @@
key=[1042] ~row amount '199.99' -> '219.99'
key=[1099] +row order_id='1099' amount='10.00' shipped_at='2026-01-15'
key=[1101] -row order_id='1101' amount='5.00' shipped_at='2026-01-12'
Exit codes
| Code | Meaning |
|---|---|
| 0 | Files are identical (or --exit-code unset, output printed) |
| 1 | Files differ (with --exit-code) |
| 2 | Parse error (bad encoding, bad CSV) |
| 3 | Schema-incompatible (ragged rows) |
Common flags
| Flag | Default | Purpose |
|---|---|---|
--key col1,col2 |
positional | Match rows by these columns |
--coerce col:kind |
none | Typed comparison (number, date, datetime, boolean, int, decimal, string) |
--dialect |
auto |
auto, excel, excel-tab, unix |
--format |
unified |
json, unified, sidebyside |
--detect-renames |
off | Detect column renames by value overlap |
--exit-code |
off | Exit code = 0/1 instead of printing the result |
Python API
from csvcomp import diff
result = diff(
"a.csv",
"b.csv",
keys=["order_id"],
coerce={"amount": "number", "shipped_at": "datetime"},
detect_renames=True,
)
result.added_columns # list[str]
result.removed_columns # list[str]
result.renamed_columns # dict[old, new]
result.added_rows # list[dict]
result.removed_rows # list[dict]
result.modified_rows # list[RowChange]
result.changed_cells # list[CellChange]
result.is_empty # bool — True if no changes at all
result.format("json") # str — JSON representation
result.format("unified") # str — git-style diff
result.format("sidebyside") # str — two-column table
Coercion kinds
| Kind | Accepts | Notes |
|---|---|---|
string |
any | byte-for-byte comparison |
number/float |
decimal text | both sides must parse; otherwise string fallback |
int |
integer text | strict int parse |
decimal |
decimal text | Decimal arithmetic, exact |
date |
ISO 8601 YYYY-MM-DD |
date.fromisoformat |
datetime |
ISO 8601 YYYY-MM-DDTHH:MM:SS |
datetime.fromisoformat |
boolean/bool |
true/false/yes/no/1/0 (case-insensitive) |
falls back to string if unparseable on both sides |
Unknown coerce kinds raise ValueError (subclass UnknownCoercionError —
also a CsvCompError).
NA semantics
The following cell values are treated as equal to "" for comparison
purposes (case-insensitive, surrounding whitespace stripped):
"", na, n/a, null, none, nan, -
This means two rows that differ only in which NA spelling they use are considered identical.
Limitations / non-goals
These are deliberately out of scope, per spec:
- No JSON / XML / Parquet / Excel support. CSV/TSV only.
- No streaming / chunked diff. Operates on in-memory dicts. Files in the ~10k row range are fast; files in the millions are not.
- No three-way diff. Two files at a time.
- No patch-file output. Read-only diff.
- No HTML / ANSI-coloured output. Three formats only: JSON, unified text, side-by-side table.
- No pandas adapter. Zero-runtime-deps invariant forbids it.
- No cursor / TUI.
- Rename detection has a 0.80 left-coverage threshold and is OFF by
default. Renames of columns whose values also legitimately appear in
another column (e.g.
country_codevscountry_iso2) may mis-detect. Seesrc/csvcomp/diff.pyfor the algorithm. - Encoding is UTF-8 only (with BOM stripping). Other encodings raise
ParseError. - Dialect sniffing is unreliable on samples <8 KB. The spec calls this
out (KeitaNakamura/diff-csv issue #1). Use
--dialectexplicitly for production data. - Trailing fully-empty rows are silently dropped. This is intentional
(matches
pandas.read_csv's defaultskip_blank_lines=True) and is documented in the spec.
Development
pip install -e ".[test]"
pytest -v
The test suite has 126 tests covering every spec acceptance criterion. See
tests/COVERAGE.md for the criterion-to-test mapping.
License
MIT. See LICENSE.
Project details
Release history Release notifications | RSS feed
Download files
Download the file for your platform. If you're not sure which to choose, learn more about installing packages.
Source Distribution
Built Distribution
Filter files by name, interpreter, ABI, and platform.
If you're not sure about the file name format, learn more about wheel file names.
Copy a direct link to the current filters
File details
Details for the file csvcomp-0.1.0.tar.gz.
File metadata
- Download URL: csvcomp-0.1.0.tar.gz
- Upload date:
- Size: 22.3 kB
- Tags: Source
- Uploaded using Trusted Publishing? No
- Uploaded via: twine/6.2.0 CPython/3.11.15
File hashes
| Algorithm | Hash digest | |
|---|---|---|
| SHA256 |
8a160c1efaa926f070f1b84682bc37f56d9bdb7536257b11b4b8495f7bc0ece3
|
|
| MD5 |
bdb950d004aff54415fff7582033b096
|
|
| BLAKE2b-256 |
01327055bacc6e5035a176980cd3470e62772bea935f27d84203280012ba96d4
|
File details
Details for the file csvcomp-0.1.0-py3-none-any.whl.
File metadata
- Download URL: csvcomp-0.1.0-py3-none-any.whl
- Upload date:
- Size: 14.3 kB
- Tags: Python 3
- Uploaded using Trusted Publishing? No
- Uploaded via: twine/6.2.0 CPython/3.11.15
File hashes
| Algorithm | Hash digest | |
|---|---|---|
| SHA256 |
ea9b33b4443e2dea35e7607c70bfcea550375441c455e2961c7e088c98664877
|
|
| MD5 |
cbcc052ef086f372c54cee1fa193b4b5
|
|
| BLAKE2b-256 |
d7c4bfa8f482547b56189b188369b0edfdb3866d8ea12cd94415501dba163df1
|