Command Line Interface¶
DataComPy ships a datacompy command so you can compare two datasets without
writing a script. It is aimed at ad hoc checks from a shell and at CI pipelines
that run as shell tasks, such as an Airflow BashOperator, a GitHub Actions
step, or a GitLab CI job.
Quick start¶
datacompy compare --left before.csv --right after.csv --on id
The command is also available as a module, which is handy when the console
script is not on PATH:
python -m datacompy compare --left before.csv --right after.csv --on id
Exit codes¶
The exit code is the contract for automation.
Code |
Meaning |
|---|---|
|
The datasets match, or stay within |
|
The datasets differ, or the threshold was exceeded |
|
Bad arguments, unreadable input, or a missing optional backend |
|
Interrupted |
Anything unexpected propagates as a traceback. Pass --debug to see the full
traceback for an error that would otherwise be reported as a short message.
Choosing a backend¶
--backend selects the comparison engine. Polars is the default.
Backend |
Use it for |
|---|---|
|
The default. Fast, in memory, no extra install |
|
Index based joins ( |
|
Distributed data. Needs |
|
Comparing two tables in place. Needs |
Inputs¶
--left and --right accept local paths and cloud URIs. CSV, Parquet, and
JSON are supported, including tab separated CSV via a .tsv extension and
newline delimited JSON via a .jsonl or .ndjson extension.
The format is inferred per file from its extension, so mixed inputs work without any extra flags:
datacompy compare --left snapshot.csv --right snapshot.parquet --on id
The extensions recognised are .csv, .tsv, .parquet, .pq,
.json, .jsonl, and .ndjson. The delimiter is inferred the same way,
a tab for .tsv and a comma for everything else, so a tab separated file
compares against a comma separated one without any extra flags.
Use --input-format when the extension is missing or unusual. It selects the
reader only and says nothing about the delimiter, so pair it with
--csv-delimiter, which overrides inference for both inputs:
datacompy compare --left extract.dat --right extract2.dat --on id \
--input-format csv --csv-delimiter '\t'
--csv-delimiter is also the way to correct a misleading extension. A comma
separated file named .tsv would otherwise be read with tabs, so force the
comma explicitly:
datacompy compare --left export.tsv --right export2.tsv --on id \
--csv-delimiter ','
A file read with the wrong delimiter collapses into a single column, which surfaces as a missing join column. The CLI warns on stderr when it sees that, naming the file and the delimiter it used.
Cloud URIs such as s3://, gs://, and abfs:// are handed straight to
the underlying reader, so they work once the matching filesystem library
(s3fs, gcsfs, adlfs) is installed.
For --backend snowflake, --left and --right are always table
references, either db.schema.table or schema.table. A two part reference
is qualified with the session’s current database. The CLI does not read local
files into Snowflake; use the pandas or polars backend for files, or load the
data into a table first.
datacompy compare --backend snowflake \
--left PROD.ANALYTICS.SALES \
--right STAGE.ANALYTICS.SALES \
--on sale_id
Join keys¶
--on accepts a comma separated list, a repeated flag, or a mix of the two:
datacompy compare --left a.csv --right b.csv --on id,date
datacompy compare --left a.csv --right b.csv --on id --on date
Use the repeated form for column names that contain a comma.
--on-index joins on the DataFrame index instead, and is only available with
--backend pandas.
Tolerances¶
--abs-tol and --rel-tol take either a single number that applies to every
numeric column, or repeated COLUMN=VALUE pairs for per column tolerances:
datacompy compare --left a.parquet --right b.parquet --on account_id \
--abs-tol 0.01 --rel-tol 0.001
datacompy compare --left a.parquet --right b.parquet --on account_id \
--abs-tol price=0.01 --abs-tol quantity=0
The two forms cannot be mixed on the same flag, because the library takes either a single tolerance or a per column mapping.
Normalisation¶
datacompy compare --left a.csv --right b.csv --on id \
--ignore-spaces --ignore-case
--ignore-extra-columns treats the datasets as matching even when one side has
columns the other does not. Column names are lowercased before comparison by
default; pass --no-cast-column-names-lower to compare them as written. That
flag does not apply to Snowflake, which normalises identifiers to uppercase
itself.
Reports¶
Rendering and destination are separate. --report-format picks between
text (the default), json, and html. --output writes to a file
instead of, or as well as, stdout.
# Human readable, to the terminal
datacompy compare --left a.csv --right b.csv --on id
# Machine readable, piped into another tool
datacompy compare --left a.csv --right b.csv --on id \
--report-format json | jq '.row_summary.unequal_rows'
# An HTML report saved for a build artifact, nothing on stdout
datacompy compare --left a.csv --right b.csv --on id \
--report-format html --output reports/diff.html --quiet
--quiet suppresses stdout only. A file named by --output is always
written, and parent directories are created as needed. The exit code is
unaffected by either flag.
--sample-count and --column-count control how many sample rows and
columns the report shows.
Failing a build¶
Without a threshold, any difference exits 1. --max-unequal-rows lets a
known amount of drift pass:
# Fail on any difference at all
datacompy compare --left before.parquet --right after.parquet --on id \
--max-unequal-rows 0 --quiet
# Tolerate up to 5 differing rows
datacompy compare --left before.parquet --right after.parquet --on id \
--max-unequal-rows 5 --quiet
By default the count includes both value mismatches and rows present in only one
dataset. Add --ignore-unique-rows to count value mismatches in common rows
only:
datacompy compare --left before.parquet --right after.parquet --on id \
--max-unequal-rows 0 --ignore-unique-rows --quiet
A threshold run also fails when one side has extra columns, unless
--ignore-extra-columns is given.
A GitHub Actions step looks like this:
- name: Check the nightly load against the previous snapshot
run: |
datacompy compare \
--left s3://warehouse/snapshots/previous.parquet \
--right s3://warehouse/snapshots/current.parquet \
--on account_id,as_of_date \
--abs-tol balance=0.01 \
--max-unequal-rows 0 \
--report-format json \
--output reports/diff.json
Backend credentials¶
Spark¶
The CLI creates its own session and stops it when the command finishes, on both
the success and the failure path. A session that already exists is borrowed and
left running, so calling the CLI from a process that owns a session is safe.
--spark-app-name sets the application name, and has no effect when a session
already exists. PySpark’s INFO and WARN logging is suppressed so it does not mix
with the report; set DATACOMPY_SPARK_LOG_LEVEL to override that.
Intermediate DataFrames are cached by default. Pass
--no-cache-intermediates on Databricks Serverless and other environments
that do not support caching.
Snowflake¶
Connection parameters come either from a JSON file or from the environment.
datacompy compare --backend snowflake \
--left PROD.ANALYTICS.SALES --right STAGE.ANALYTICS.SALES --on sale_id \
--snowflake-config ~/.snowflake/connection.json
The JSON file holds Snowpark connection parameters as top level keys, for
example account, user, password, role, warehouse,
database, and schema.
Without --snowflake-config, the session is built from the environment:
Variable |
Notes |
|---|---|
|
Required |
|
Required, except under OAuth |
|
Required unless a token or authenticator is set |
|
OAuth access token; implies OAuth on its own |
|
|
|
Optional |
|
Optional |
|
Optional, qualifies two part references |
|
Optional |
Full option list¶
datacompy compare --help