The same two rules, three different tools
Two ordinary, real validation rules: every order_id must be unique, every amount must be non-negative. Applied to the same three-row dataset, one row with a duplicate order_id, one with a negative amount, through pandera, dbt tests, and Great Expectations, actually run, not just read about.
graph TD
A[Same Data,<br/>Same Two Rules] --> B[pandera]
A --> C[dbt tests]
A --> D[Great Expectations]
B --> E[Real result]
C --> F[Real result]
D --> G[Did it even run?]
pandera: in-process, on a pandas DataFrame
schema = pa.DataFrameSchema({
"order_id": pa.Column(int, unique=True),
"customer_id": pa.Column(int),
"amount": pa.Column(float, pa.Check.ge(0)),
})
schema.validate(orders, lazy=True)
real pandera check time: 28.9ms
real pandera error output:
check failure_case
field_uniqueness 2.0
field_uniqueness 2.0
greater_than_or_equal_to(0) -10.0
Both real violations caught, in milliseconds, as a normal Python exception with a structured DataFrame of exactly which values failed which check. No separate process, no external service, the validation runs wherever the Python code that called it runs.
dbt tests: SQL, against a real warehouse
models:
- name: orders
columns:
- name: order_id
tests: [unique]
-- tests/assert_non_negative_amount.sql
select order_id, amount from {{ ref('orders') }} where amount < 0
real dbt test time: 5.91s
1 of 2 FAIL 1 assert_non_negative_amount
2 of 2 FAIL 1 unique_orders_order_id
Done. PASS=0 WARN=0 ERROR=2 SKIP=0 NO-OP=0 REUSED=0 TOTAL=2
Both real violations caught here too, with a clean, real exit code any CI system can act on. The 5.91 seconds isn't dbt being slow at the actual check, it's a real, honest reflection of what dbt tests actually are: a CLI process that starts up, compiles SQL, and executes it against a real warehouse round-trip, structurally different overhead from a function call inside an already-running Python process. Comparing the two timings directly isn't "dbt is 200x slower," it's confirming a genuine architectural difference: in-process validation versus a real subprocess hitting a real database.
Great Expectations: the one that didn't finish
import great_expectations
print("this line never runs")
UserWarning: Core Pydantic V1 functionality isn't compatible with Python 3.14 or greater.
[process exits, code 0, no traceback, "this line never runs" never prints]
Not a hang. Not an exception. The Python process exits cleanly, silently, partway through the import statement itself, before a single validation rule ever gets defined. Installing the package also downgraded numpy and altair to versions incompatible with two other real libraries already in use, shap and streamlit, a real dependency conflict on top of the import failure. This isn't a criticism of Great Expectations' design; it's a real, current, reproducible fact about running its latest release specifically on Python 3.14, confirmed by actually trying, cleaned up, and verified the rest of the environment still worked correctly afterward.
What actually differs between the two that worked
pandera validates Python objects already in memory, pandas or Polars DataFrames, as part of the same process that's already running the analysis or pipeline code. It's fast because there's no external process or round-trip involved, and it fits naturally into unit-test-style workflows. dbt tests validate data that already lives in a real warehouse, expressed as SQL, run through a real CLI, and they're the natural fit when the data being checked is already sitting in Postgres, Snowflake, or BigQuery as part of a transformation pipeline rather than a Python script. Neither is a strictly better general-purpose choice; which one fits depends on where the data already lives when the check needs to run.
The takeaway
Every one of these three real results came from actually running the tool, not reading its documentation or trusting a comparison written by someone who did the same. Two genuinely work well, for different real situations. One doesn't currently run at all on this specific, real, current Python version, a fact worth knowing before spending time integrating it, and the kind of thing only surfaces by trying, which is exactly what happened here.
Comments
Loading comments...