typedframes 0.3.1__tar.gz → 0.4.0__tar.gz

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (38) hide show
  1. {typedframes-0.3.1 → typedframes-0.4.0}/PKG-INFO +249 -96
  2. {typedframes-0.3.1 → typedframes-0.4.0}/README.md +248 -95
  3. {typedframes-0.3.1 → typedframes-0.4.0}/pyproject.toml +36 -23
  4. typedframes-0.4.0/rust/.cargo/config.toml +21 -0
  5. {typedframes-0.3.1 → typedframes-0.4.0}/rust/Cargo.lock +495 -241
  6. {typedframes-0.3.1 → typedframes-0.4.0}/rust/Cargo.toml +18 -8
  7. {typedframes-0.3.1 → typedframes-0.4.0}/rust/benches/parser_bench.rs +2 -2
  8. typedframes-0.4.0/rust/src/ast_extract.rs +644 -0
  9. typedframes-0.4.0/rust/src/config.rs +199 -0
  10. typedframes-0.4.0/rust/src/constants.rs +248 -0
  11. typedframes-0.4.0/rust/src/contract.rs +200 -0
  12. typedframes-0.4.0/rust/src/errors.rs +103 -0
  13. typedframes-0.4.0/rust/src/index.rs +1715 -0
  14. typedframes-0.4.0/rust/src/lib.rs +57 -0
  15. typedframes-0.4.0/rust/src/linter.rs +6607 -0
  16. typedframes-0.4.0/rust/src/notebook.rs +270 -0
  17. typedframes-0.4.0/rust/src/pyapi.rs +251 -0
  18. typedframes-0.4.0/rust/src/sql.rs +774 -0
  19. typedframes-0.4.0/rust/src/typo.rs +90 -0
  20. {typedframes-0.3.1 → typedframes-0.4.0}/rust/tests/integration_test.rs +0 -62
  21. {typedframes-0.3.1 → typedframes-0.4.0}/src/typedframes/__init__.py +1 -1
  22. typedframes-0.4.0/src/typedframes/cli.py +1023 -0
  23. {typedframes-0.3.1 → typedframes-0.4.0}/src/typedframes/column_group.py +6 -3
  24. {typedframes-0.3.1 → typedframes-0.4.0}/src/typedframes/column_set.py +5 -2
  25. {typedframes-0.3.1 → typedframes-0.4.0}/src/typedframes/mypy.py +2 -1
  26. {typedframes-0.3.1 → typedframes-0.4.0}/src/typedframes/pandas.py +22 -1
  27. {typedframes-0.3.1 → typedframes-0.4.0}/src/typedframes/polars.py +43 -4
  28. typedframes-0.3.1/rust/src/lib.rs +0 -4669
  29. typedframes-0.3.1/src/typedframes/cli.py +0 -202
  30. {typedframes-0.3.1 → typedframes-0.4.0}/rust/README.md +0 -0
  31. {typedframes-0.3.1 → typedframes-0.4.0}/rust/src/main.rs +0 -0
  32. {typedframes-0.3.1 → typedframes-0.4.0}/src/typedframes/base_schema.py +0 -0
  33. {typedframes-0.3.1 → typedframes-0.4.0}/src/typedframes/column.py +0 -0
  34. {typedframes-0.3.1 → typedframes-0.4.0}/src/typedframes/column_group_error.py +0 -0
  35. {typedframes-0.3.1 → typedframes-0.4.0}/src/typedframes/missing_dependency_error.py +0 -0
  36. {typedframes-0.3.1 → typedframes-0.4.0}/src/typedframes/pandera.py +0 -0
  37. {typedframes-0.3.1 → typedframes-0.4.0}/src/typedframes/py.typed +0 -0
  38. {typedframes-0.3.1 → typedframes-0.4.0}/src/typedframes/schema_algebra.py +0 -0
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: typedframes
3
- Version: 0.3.1
3
+ Version: 0.4.0
4
4
  Classifier: Development Status :: 3 - Alpha
5
5
  Classifier: Intended Audience :: Developers
6
6
  Classifier: License :: OSI Approved :: MIT License
@@ -42,32 +42,32 @@ Project-URL: Repository, https://github.com/w-martin/typedframes
42
42
 
43
43
  > ⚠️ **Project Status: Proof of Concept**
44
44
  >
45
- > `typedframes` (v0.3.1) is currently an experimental proof-of-concept. The core static analysis and mypy/Rust
45
+ > `typedframes` (v0.4.0) is currently an experimental proof-of-concept. The core static analysis and mypy/Rust
46
46
  > integrations work, but expect rough edges. The codebase prioritizes demonstrating the viability of static DataFrame
47
- > schema validation over production-grade stability.
47
+ > column checking over production-grade stability.
48
48
  >
49
- **Static analysis for pandas and polars DataFrames. Catch column errors at lint-time, not runtime.**
49
+ **A Rust-fast linter for pandas and polars DataFrames. Catches column errors at lint-time, and gates CI on how
50
+ much of your codebase it can actually see — no schema classes required to start.**
50
51
 
51
52
  ```python
52
- from typing import Annotated
53
53
  import pandas as pd
54
- from typedframes import BaseSchema, Column
55
-
56
-
57
- class UserData(BaseSchema):
58
- user_id = Column(type=int)
59
- email = Column(type=str)
60
- signup_date = Column(type=str)
61
54
 
55
+ # Checker infers {order_id, amount, status} from usecols= — no schema class needed
56
+ orders = pd.read_csv("orders.csv", usecols=["order_id", "amount", "status"])
57
+ print(orders["amount"]) # ✓ OK
58
+ print(orders["revenue"]) # ✗ unknown-column — 'revenue' not in inferred column set
59
+ ```
62
60
 
63
- df: Annotated[pd.DataFrame, UserData] = pd.read_csv("users.csv")
64
- df['user_id'] # ✓ Validated by checker
65
- df['username'] # ✗ unknown-column: Column 'username' not in UserData
61
+ ```shell
62
+ typedframes check src/ --fail-under=90
63
+ # src/pipeline.py:7:8: error[unknown-column] Column 'revenue' does not exist in inferred column set (defined at line 6)
64
+ # ✗ Found 1 error in 12 files (0.0s)
65
+ # ✗ DataFrame schema coverage 82.0% is below the required 90.0% (9/11 DataFrames had column info)
66
66
  ```
67
67
 
68
- **Descriptors as a bridge:** define once in `Column(type=int)`, access as `df[UserData.user_id.s]` (pandas string
69
- access) or `df.select(UserData.revenue.col)` (polars expression). Refactor by changing the descriptor definition —
70
- all `.s` and `.col` references update automatically. No find-and-replace across string literals.
68
+ `--fail-under=N` is the same idea as a test-coverage or mypy type-coverage gate, applied to how many DataFrames
69
+ the checker can resolve columns for — see [DataFrame Schema Coverage Thresholds](#dataframe-schema-coverage-thresholds-opt-in).
70
+ Add `BaseSchema` classes later for cross-file awareness and IDE autocomplete — see [Quick Start](#quick-start).
71
71
 
72
72
  ---
73
73
 
@@ -78,6 +78,7 @@ all `.s` and `.col` references update automatically. No find-and-replace across
78
78
  - [Quick Start](#quick-start)
79
79
  - [Column Inference](#column-inference)
80
80
  - [Static Analysis](#static-analysis)
81
+ - [DataFrame Schema Coverage Thresholds](#dataframe-schema-coverage-thresholds-opt-in)
81
82
  - [Static Analysis Performance](#static-analysis-performance)
82
83
  - [Type Safety With Multiple Backends](#type-safety-with-multiple-backends)
83
84
  - [Advanced Usage](#advanced-usage)
@@ -91,15 +92,21 @@ all `.s` and `.col` references update automatically. No find-and-replace across
91
92
 
92
93
  ## Why typedframes?
93
94
 
94
- **The problem:** Many pandas bugs are column mismatches. You access a column that doesn't exist, pass the wrong schema to a function, or make a typo. These errors only surface at runtime, often in production.
95
+ **The problem:** Many pandas bugs are column mismatches — you access a column that doesn't exist, pass a
96
+ DataFrame missing a column a function needs, or make a typo. These errors only surface at runtime, often in
97
+ production, and it's hard to know how much of a codebase is actually protected against them.
95
98
 
96
- **The solution:** Define your DataFrame schemas as Python classes. Get static type checking that catches column errors before you even run your code.
99
+ **The solution:** A fast standalone linter that infers column sets from your existing code (`usecols=`,
100
+ `dtype=`, method chains) and catches mismatches at lint-time — plus an opt-in coverage threshold so CI fails
101
+ when too much of the codebase is invisible to the checker. Add `BaseSchema` classes where you want cross-file
102
+ tracking and IDE autocomplete; they're a progressive enhancement, not a prerequisite.
97
103
 
98
104
  **What you get:**
99
105
 
100
106
  - ✅ **Works without schema annotations** - Column inference from `usecols=`, `dtype=`, and method chains catches errors on unannotated code
107
+ - ✅ **CI gate via coverage threshold** - `--fail-under=N` fails the build when too much of your codebase is invisible to the checker, the same way you'd gate on test coverage or a type checker's type-coverage number
108
+ - ✅ **Rust-fast** - Milliseconds, not seconds, even on hundreds of files; fast enough for pre-commit hooks and CI (see [benchmarks](#static-analysis-performance))
101
109
  - ✅ **Cross-file awareness** - Add `BaseSchema` and typed return annotations to follow schemas across module boundaries
102
- - ✅ **Static analysis** - Catch column errors at lint-time with mypy or the standalone checker
103
110
  - ✅ **Refactor-safe access** - `df[Schema.column_group.s].mean()` (pandas) or `df.select(Schema.col.col)` (polars) instead of scattered string literals
104
111
  - ✅ **Works with pandas AND polars** - Same schema API, native backend types
105
112
  - ✅ **Dynamic column matching** - Regex-based ColumnSets for time-series data
@@ -134,7 +141,7 @@ import pandas as pd
134
141
 
135
142
  # Checker infers {order_id, amount, status} from usecols=
136
143
  orders = pd.read_csv("orders.csv", usecols=["order_id", "amount", "status"])
137
- print(orders["amount"]) # ✓ OK
144
+ print(orders["amount"]) # ✓ OK
138
145
  print(orders["revenue"]) # ✗ unknown-column — 'revenue' not in inferred set
139
146
  ```
140
147
 
@@ -144,13 +151,17 @@ typedframes check src/
144
151
  # ✗ Found 1 error in 12 files (0.0s)
145
152
  ```
146
153
 
147
- See [`examples/multi_file_inference/`](examples/multi_file_inference/) for a multi-file example with no `BaseSchema`
154
+ See [`examples/features/multi_file_inference/`](examples/features/multi_file_inference/) for a multi-file example with no `BaseSchema`
148
155
  classes at all.
149
156
 
150
157
  ### Define Your Schema (Once)
151
158
 
152
159
  Add `BaseSchema` classes when you want cross-file awareness and IDE autocomplete. Schemas travel with function return
153
- types across module boundaries — the checker validates call sites even in files that have no `usecols=` of their own:
160
+ types across module boundaries — the checker validates call sites even in files that have no `usecols=` of their own.
161
+
162
+ **Descriptors as a bridge:** define once in `Column(type=int)`, access as `df[UserData.user_id.s]` (pandas string
163
+ access) or `df.select(UserData.revenue.col)` (polars expression). Refactor by changing the descriptor definition —
164
+ all `.s` and `.col` references update automatically. No find-and-replace across string literals.
154
165
 
155
166
  ```python
156
167
  from typedframes import BaseSchema, Column, ColumnSet
@@ -175,17 +186,17 @@ import pandas as pd
175
186
  df: Annotated[pd.DataFrame, SalesData] = pd.read_csv("sales.csv")
176
187
 
177
188
  # String access — validated by the standalone checker
178
- print(df['revenue'].sum())
179
- print(df['profit']) # ✗ unknown-column: Column 'profit' not in SalesData
189
+ print(df["revenue"].sum())
190
+ print(df["profit"]) # ✗ unknown-column: Column 'profit' not in SalesData
180
191
 
181
192
  # .s gives a refactor-safe string name from the descriptor
182
- print(df[SalesData.revenue.s].sum()) # same as df['revenue'].sum()
193
+ print(df[SalesData.revenue.s].sum()) # same as df['revenue'].sum()
183
194
 
184
195
 
185
196
  # Type-safe function signature
186
197
  def analyze(data: Annotated[pd.DataFrame, SalesData]) -> float:
187
- data['revenue'] # ✓ Validated by checker
188
- data['profit'] # ✗ unknown-column: 'profit' not in SalesData
198
+ data["revenue"] # ✓ Validated by checker
199
+ data["profit"] # ✗ unknown-column: 'profit' not in SalesData
189
200
  return data[SalesData.revenue.s].mean()
190
201
  ```
191
202
 
@@ -199,12 +210,12 @@ import polars as pl
199
210
  df: Annotated[pl.DataFrame, SalesData] = pl.read_csv("sales.csv")
200
211
 
201
212
  # pl.col() references are now validated by the standalone checker
202
- print(df.filter(pl.col('revenue') > 1000))
203
- print(df.select(pl.col('profit'))) # ✗ unknown-column: Column 'profit' not in SalesData
213
+ print(df.filter(pl.col("revenue") > 1000))
214
+ print(df.select(pl.col("profit"))) # ✗ unknown-column: Column 'profit' not in SalesData
204
215
 
205
216
  # .col gives a refactor-safe polars expression from the descriptor
206
217
  filtered = df.filter(SalesData.revenue.col > 1000)
207
- grouped = df.group_by('customer_id').agg(SalesData.revenue.col.sum())
218
+ grouped = df.group_by("customer_id").agg(SalesData.revenue.col.sum())
208
219
  ```
209
220
 
210
221
  ---
@@ -224,7 +235,7 @@ and validates all subscript access against it — no schema annotation required:
224
235
  ```python
225
236
  # Checker infers {user_id, email} from usecols= — no annotation needed
226
237
  df = pd.read_csv("users.csv", usecols=["user_id", "email"])
227
- print(df["user_id"]) # ✓ OK — in usecols
238
+ print(df["user_id"]) # ✓ OK — in usecols
228
239
  # print(df["age"]) # ✗ Error: 'age' not in inferred column set
229
240
  ```
230
241
 
@@ -234,6 +245,7 @@ unchanged. Structural operations update it:
234
245
 
235
246
  ```python
236
247
  from typing import Annotated
248
+
237
249
  df: Annotated[pd.DataFrame, UserData] = pd.read_csv("users.csv")
238
250
 
239
251
  # Subscript slice — inferred column set {user_id, email}
@@ -255,29 +267,31 @@ print(augmented["created_at"]) # ✓ OK
255
267
 
256
268
  ### Inference Gaps and Warnings
257
269
 
258
- **untracked-dataframe — unannotated data ingestion (off by default)**
270
+ **untracked-dataframe — unannotated data ingestion (on by default)**
259
271
 
260
- By default, typedframes supports permissive Exploratory Data Analysis (EDA). When a DataFrame is loaded
261
- via `pd.read_csv()` without `usecols=` or a schema annotation, the checker assumes an *Unknown* state
262
- and bypasses strict column validation to avoid nagging you during discovery.
272
+ When a DataFrame is loaded via `pd.read_csv()` without `usecols=` or a schema annotation, the checker
273
+ assumes an *Unknown* state, bypasses strict column validation on it (to avoid false positives on columns
274
+ it simply can't see), and flags the load itself as a warning-level diagnostic.
263
275
 
264
- To lock down production CI/CD pipelines, opt in to `untracked-dataframe` warnings with `--strict-ingest`:
276
+ For permissive Exploratory Data Analysis (EDA) work where you don't want that noise yet, downgrade it to
277
+ a quiet info-level note with `--lenient-ingest`:
265
278
 
266
279
  ```shell
267
- typedframes check src/ --strict-ingest
280
+ typedframes check src/ --lenient-ingest
268
281
  ```
269
282
 
270
- With strict ingestion enabled, loading a DataFrame without a schema or `usecols=` produces:
283
+ By default, loading a DataFrame without a schema or `usecols=` produces:
271
284
 
272
285
  ```python
273
286
  df = pd.read_csv("users.csv")
274
- # ⚠ untracked-dataframe: columns unknown at lint time; specify `usecols`/`columns` or
275
- # annotate: `df: Annotated[pd.DataFrame, MySchema] = pd.read_csv(...)`
287
+ # ⚠ untracked-dataframe: columns unknown at lint time; specify `usecols`/`columns`, or
288
+ # annotate the variable's type, e.g. `df: Annotated[pd.DataFrame, MySchema] = pd.read_csv(...)`
276
289
  ```
277
290
 
278
291
  Fix option 1 — annotate with a schema:
279
292
  ```python
280
293
  from typing import Annotated
294
+
281
295
  df: Annotated[pd.DataFrame, UserData] = pd.read_csv("users.csv")
282
296
  ```
283
297
 
@@ -292,6 +306,7 @@ Emitted when `drop(columns=[...])` names a column that isn't in the inferred set
292
306
 
293
307
  ```python
294
308
  from typing import Annotated
309
+
295
310
  df: Annotated[pd.DataFrame, UserData] = pd.read_csv("users.csv")
296
311
  trimmed = df.drop(columns=["nonexistent"])
297
312
  # ⚠ dropped-unknown-column: Dropped column 'nonexistent' does not exist in UserData
@@ -339,13 +354,28 @@ Column-list slices (`df[["a", "b"]]`) contribute to the contract too.
339
354
 
340
355
  ### See Also
341
356
 
342
- - [`examples/inference_example.py`](examples/inference_example.py) — single-file walkthrough of all four inference
357
+ - [`examples/features/inference_example.py`](examples/features/inference_example.py) — single-file walkthrough of all four inference
343
358
  scenarios with annotated ✓/✗ comments.
344
- - [`examples/multi_file_inference/`](examples/multi_file_inference/) — multi-file project checked with
345
- `typedframes check examples/multi_file_inference/`; no `BaseSchema` anywhere. Includes a function
359
+ - [`examples/features/multi_file_inference/`](examples/features/multi_file_inference/) — multi-file project checked with
360
+ `typedframes check examples/features/multi_file_inference/`; no `BaseSchema` anywhere. Includes a function
346
361
  parameter contract violation caught at the call site (`missing-column`).
347
- - [`examples/multi_file_with_schema/`](examples/multi_file_with_schema/) — same scenario with `BaseSchema`
362
+ - [`examples/features/multi_file_with_schema/`](examples/features/multi_file_with_schema/) — same scenario with `BaseSchema`
348
363
  classes; the checker follows schemas across module boundaries via the project index.
364
+ - SQL / data-warehouse column inference — the column set is inferred from a query's `SELECT` list instead of
365
+ `usecols=`/`columns=`, including tracing the query back through a single-assignment variable or a `.sql` file,
366
+ and dialect-aware identifier case folding (`sql_dialect` in `pyproject.toml` — see [Project-level
367
+ configuration](docs/api/cli.md#project-level-configuration)):
368
+ [`examples/sql_connectors/snowflake/`](examples/sql_connectors/snowflake/), [`examples/sql_connectors/bigquery/`](examples/sql_connectors/bigquery/),
369
+ [`examples/sql_connectors/athena/`](examples/sql_connectors/athena/), [`examples/sql_connectors/redshift/`](examples/sql_connectors/redshift/),
370
+ [`examples/sql_connectors/databricks/`](examples/sql_connectors/databricks/), [`examples/sql_connectors/pyspark/`](examples/sql_connectors/pyspark/),
371
+ [`examples/sql_connectors/duckdb/`](examples/sql_connectors/duckdb/), [`examples/sql_connectors/connectorx/`](examples/sql_connectors/connectorx/),
372
+ [`examples/sql_connectors/sqlalchemy/`](examples/sql_connectors/sqlalchemy/) (Core `select()` and declarative models, not just raw SQL text),
373
+ [`examples/sql_connectors/feast/`](examples/sql_connectors/feast/) (feature-store retrieval, registered as an *open* schema since
374
+ `entity_df`'s own columns aren't enumerable in general), and
375
+ [`examples/sql_connectors/azure_synapse/`](examples/sql_connectors/azure_synapse/) (Azure's closest analog to Athena, including T-SQL's
376
+ `[bracket-quoted]` identifier convention). A wrapper function that case-folds a connector's result before
377
+ returning it (`.rename(columns=str.lower)`, `df.columns = df.columns.str.lower()`) is traced cross-file
378
+ too — see [docs/usage.md's "Supported column-set transforms"](docs/usage.md#supported-column-set-transforms).
349
379
 
350
380
  ---
351
381
 
@@ -390,13 +420,19 @@ typedframes check src/
390
420
  typedframes check src/ --strict
391
421
 
392
422
  # JSON output
393
- typedframes check src/ --json
423
+ typedframes check src/ --output-format=json
394
424
 
395
425
  # Skip cross-file index (single-file mode, faster for quick checks)
396
426
  typedframes check src/ --no-index
397
427
 
398
428
  # Suppress all warnings (untracked-dataframe, dropped-unknown-column)
399
429
  typedframes check src/ --no-warnings
430
+
431
+ # Enforce minimum DataFrame schema coverage (see below)
432
+ typedframes check src/ --fail-under=90
433
+
434
+ # Show which DataFrames lack column info, per file
435
+ typedframes check src/ --coverage-report=term-missing
400
436
  ```
401
437
 
402
438
  To suppress warnings project-wide, add to `pyproject.toml`:
@@ -446,22 +482,126 @@ code reference.
446
482
 
447
483
  ---
448
484
 
485
+ ## DataFrame Schema Coverage Thresholds (Opt-In)
486
+
487
+ **DataFrame schema coverage** is the fraction of DataFrames `typedframes check` could
488
+ resolve column information for — the analogue of the "type coverage" reported by mypy,
489
+ pyright, and pyre, and unrelated to test coverage. That number is informational by
490
+ default. If you want it enforced — failing the run when too much of your code is
491
+ invisible to the checker — enable a threshold.
492
+
493
+ This is **entirely opt-in**. With no `[tool.typedframes.coverage]` table and no
494
+ `--fail-under`, nothing changes: no threshold, no exit-code difference.
495
+
496
+ Every supported key, at its default value:
497
+
498
+ ```toml
499
+ [tool.typedframes.coverage]
500
+ # Master switch. Coverage enforcement is off unless this is true, so adding this
501
+ # table without setting it changes nothing.
502
+ enabled = false
503
+
504
+ # Minimum percentage of DataFrames that must have recognized column/schema info
505
+ # before `typedframes check` exits 1. Only consulted when `enabled = true`.
506
+ # Applies to every file not captured by a glob in [overrides] below.
507
+ fail_under = 100.0
508
+
509
+ # How much coverage detail to print after each check. One of:
510
+ # "summary" one line (the default, unchanged from before this feature)
511
+ # "term-missing" per-file table plus the DataFrame sites lacking column info
512
+ # "json" machine-readable document, for CI tooling
513
+ # Independent of `enabled` — a detailed report is useful without a gate, and
514
+ # vice versa. Overridden by `--coverage-report`.
515
+ report = "summary"
516
+
517
+ [tool.typedframes.coverage.overrides]
518
+ # Per-path glob overrides of `fail_under`, for holding legacy code to a lower bar
519
+ # than new code. Each glob is graded on its own files as a separate group, so a
520
+ # lenient legacy bucket can't drag down (or rescue) the rest of the project.
521
+ # Paths are matched project-relative: `**` spans any number of directories,
522
+ # `*` and `?` stay within one path segment.
523
+ # When several globs match one file the most specific wins — longest literal
524
+ # prefix before the first `*` or `?`. Files matching no glob use `fail_under`.
525
+ # "legacy/**" = 50.0
526
+ # "src/new_module/**" = 100.0
527
+ ```
528
+
529
+ Prefer to keep `pyproject.toml` clean? The same settings work in a standalone
530
+ `typedframes.toml` at the project root, with the `[tool.typedframes]` prefix dropped
531
+ (the way `ruff.toml` drops `[tool.ruff]`):
532
+
533
+ ```toml
534
+ # typedframes.toml
535
+ [coverage]
536
+ enabled = false
537
+ fail_under = 100.0
538
+ report = "summary"
539
+
540
+ [coverage.overrides]
541
+ # "legacy/**" = 50.0
542
+ ```
543
+
544
+ If both files exist, `typedframes.toml` wins **entirely** — the two are never merged,
545
+ so exactly one file explains the whole configuration.
546
+
547
+ ### Seeing What's Missing
548
+
549
+ The default one-line DataFrame schema coverage summary tells you the ratio but not what
550
+ to fix. `--coverage-report=term-missing` names the DataFrames that cost you coverage:
551
+
552
+ ```shell
553
+ typedframes check src/ --coverage-report=term-missing
554
+ ```
555
+
556
+ ```
557
+ Name Typed Total Cover Missing
558
+ ---------------------------------------------
559
+ legacy/old.py 0 2 0% old_one:2, old_two:3
560
+ src/new.py 1 2 50% bad:3
561
+ ---------------------------------------------
562
+ TOTAL 1 4 25%
563
+ ```
564
+
565
+ Each `Missing` entry is `variable:line` — the assignment where the checker recognized a
566
+ DataFrame but couldn't resolve its columns. Fix those (add `usecols=`, name the columns
567
+ in the `SELECT`, or annotate the variable) and coverage rises.
568
+
569
+ For CI tooling, `--coverage-report=json` emits the same data as a document. Combine it
570
+ with `--output-format=json` and the coverage report is nested under a `coverage` key so
571
+ stdout stays a single valid JSON document:
572
+
573
+ ```shell
574
+ typedframes check src/ --output-format=json --coverage-report=json
575
+ ```
576
+
577
+ Notes:
578
+
579
+ - Coverage is a **separate gate from `--strict`**. `--strict` fails on errors
580
+ (correctness); a threshold fails on missing column information (completeness).
581
+ Enabling one never implies the other.
582
+ - `--fail-under=N` is a **total override**: it applies one threshold everywhere and
583
+ ignores the config table, per-path overrides included. Handy for a one-off CI run.
584
+ - A group with no recognized DataFrames passes: 0/0 means there was nothing to
585
+ measure, not that something failed.
586
+ - A failed threshold exits **1** and is reported even under `--no-info` — that flag
587
+ silences the informational summary line, not a gate result.
588
+
449
589
  ## Static Analysis Performance
450
590
 
451
591
  Fast feedback reduces development time. The typedframes Rust binary provides near-instant column checking.
452
592
 
453
593
  **Benchmark results** (20 runs, 3 warmup, caches cleared between runs):
454
- *2026-07-13 · Darwin 25.5.0 · arm · CPython 3.14.4 · 64GiB RAM · Great Expectations pinned @ 1.9.3*
594
+ *2026-08-12 · Darwin 25.6.0 · arm · CPython 3.14.4 · 64GiB RAM · Great Expectations pinned @ 1.9.3*
455
595
 
456
596
  | Tool | Version | What it does | typedframes (13 files) | great_expectations (482 files) |
457
597
  |------|---------|--------------|------------------------|--------------------------------|
458
- | typedframes | 0.3.1 | DataFrame column checker | 43ms ±668µs | 213ms ±4ms |
459
- | ruff | 0.15.21 | Linter (no type checking) | 26ms ±1ms | 193ms ±2ms |
460
- | ty | 0.0.58 | Type checker | 72ms ±3ms | 181ms ±9ms |
461
- | pyrefly | 1.1.1 | Type checker | 107ms ±4ms | 557ms ±16ms |
462
- | mypy | 2.2.0 | Type checker (no plugin) | 2.77s ±41ms | 4.21s ±71ms |
463
- | mypy + typedframes | 2.2.0 | Type checker + column checker | 2.78s ±34ms | 4.48s ±63ms |
464
- | pyright | 1.1.411 | Type checker | 743ms ±8ms | 3.34s ±43ms |
598
+ | typedframes | 0.4.0 | DataFrame column checker | 47ms ±998µs (IQR 1ms) | 183ms ±2ms (IQR 2ms) |
599
+ | ruff | 0.16.2 | Linter (no type checking) | 29ms ±677µs (IQR 729µs) | 208ms ±2ms (IQR 3ms) |
600
+ | ty | 0.0.69 | Type checker | 74ms ±900µs (IQR 1ms) | 941ms ±14ms (IQR 18ms) |
601
+ | pyrefly | 1.2.0 | Type checker | 96ms ±2ms (IQR 2ms) | 275ms ±7ms (IQR 14ms) |
602
+ | mypy | 2.3.0 | Type checker (no plugin) | 2.78s ±47ms (IQR 84ms) | 4.25s ±48ms (IQR 76ms) |
603
+ | mypy + typedframes | 2.3.0 | Type checker + column checker | 2.74s ±29ms (IQR 34ms) | 4.44s ±54ms (IQR 105ms) |
604
+ | pyright | 1.1.411 | Type checker | 808ms ±24ms (IQR 21ms) | 3.45s ±93ms (IQR 138ms) |
465
605
 
466
606
  *Run `uv run python benchmarks/benchmark_checkers.py` to reproduce.*
467
607
 
@@ -496,12 +636,12 @@ class UserData(BaseSchema):
496
636
 
497
637
  # Pandas pipeline - type checker knows pandas methods
498
638
  def pandas_analyze(df: Annotated[pd.DataFrame, UserData]) -> Annotated[pd.DataFrame, UserData]:
499
- return df[df['user_id'] > 100] # ✓ Pandas syntax
639
+ return df[df["user_id"] > 100] # ✓ Pandas syntax
500
640
 
501
641
 
502
642
  # Polars pipeline - type checker knows polars methods
503
643
  def polars_analyze(df: Annotated[pl.DataFrame, UserData]) -> Annotated[pl.DataFrame, UserData]:
504
- return df.filter(pl.col('user_id') > 100) # ✓ Polars syntax
644
+ return df.filter(pl.col("user_id") > 100) # ✓ Polars syntax
505
645
 
506
646
 
507
647
  # Use native types throughout
@@ -541,7 +681,7 @@ class OrderSchema(BaseSchema):
541
681
 
542
682
  # Schema preserved through filtering
543
683
  def get_active_users(df: Annotated[pd.DataFrame, UserSchema]) -> Annotated[pd.DataFrame, UserSchema]:
544
- return df[df['user_id'] > 100] # ✓ Validated by checker
684
+ return df[df["user_id"] > 100] # ✓ Validated by checker
545
685
 
546
686
 
547
687
  # Schema preserved through merges
@@ -558,7 +698,7 @@ import polars as pl
558
698
 
559
699
  # Schema columns work in filter expressions
560
700
  def filter_users(df: Annotated[pl.DataFrame, UserSchema]) -> pl.DataFrame:
561
- return df.filter(pl.col('user_id') > 100)
701
+ return df.filter(pl.col("user_id") > 100)
562
702
 
563
703
 
564
704
  # Schema columns work in join expressions
@@ -597,7 +737,7 @@ class SensorReadings(BaseSchema):
597
737
 
598
738
 
599
739
  df: Annotated[pd.DataFrame, SensorReadings] = pd.read_csv("readings.csv")
600
- df[SensorReadings.sensors.s].mean() # ✓ Expands to df[["sensor_1", "sensor_2", "sensor_3"]].mean()
740
+ df[SensorReadings.sensors.s].mean() # ✓ Expands to df[["sensor_1", "sensor_2", "sensor_3"]].mean()
601
741
  ```
602
742
 
603
743
  For logical grouping across multiple ColumnSets:
@@ -648,6 +788,7 @@ class Orders(BaseSchema):
648
788
  # Combine via multiple inheritance
649
789
  class UserOrders(UserPublic, Orders):
650
790
  """Type checkers see all columns from both parents."""
791
+
651
792
  ...
652
793
 
653
794
 
@@ -661,7 +802,7 @@ merged: Annotated[pd.DataFrame, UserOrders] = users.merge(orders, on=UserPublic.
661
802
 
662
803
  Overlapping columns with the same type are allowed (common after merges). Conflicting types raise `SchemaConflictError`.
663
804
 
664
- See [`examples/schema_algebra_example.py`](examples/schema_algebra_example.py) for a complete walkthrough.
805
+ See [`examples/features/schema_algebra_example.py`](examples/features/schema_algebra_example.py) for a complete walkthrough.
665
806
 
666
807
  ---
667
808
 
@@ -674,12 +815,12 @@ Comprehensive comparison of pandas/DataFrame typing and validation tools. **type
674
815
 
675
816
  | Feature | typedframes | Pandera | Great Expectations | strictly_typed_pandas | pandas-stubs | dataenforce | pandas-type-checks | StaticFrame | narwhals | dataframely | patito |
676
817
  |---------------------------------|------------------------|-------------|--------------------|-----------------------|--------------|-------------|--------------------|------------------|----------|------------------|------------------|
677
- | **Version tested** | 0.3.1 | 0.32.1 | 1.18.2 | 0.3.7 | 3.0.3 | 0.1.2 | 1.1.3 | 5.0.0 | 2.23.0 | 2.13.0 | 0.8.6 |
818
+ | **Version tested** | 0.4.0 | 0.32.1 | 1.19.1 | 0.3.7 | 3.0.5 | 0.1.2 | 1.1.3 | 5.0.0 | 2.24.0 | 3.0.0 | 0.8.6 |
678
819
  | **Analysis Type** |
679
820
  | When errors are caught | **Static (lint-time)** | Runtime | Runtime | Runtime | Static | Runtime | Runtime | Runtime | Runtime | Runtime | Runtime |
680
821
  | **Static Analysis (our focus)** |
681
822
  | Mypy plugin | ✅ Yes | ⚠️ Limited | ❌ No | ❌ No | ✅ Yes | ❌ No | ❌ No | ⚠️ Basic | ❌ No | ❌ No | ❌ No |
682
- | Standalone checker | ✅ Rust (~1ms) | ❌ No | ❌ No | ❌ No | ❌ No | ❌ No | ❌ No | ❌ No | ❌ No | ❌ No | ❌ No |
823
+ | Standalone checker | ✅ Rust (ms-scale) | ❌ No | ❌ No | ❌ No | ❌ No | ❌ No | ❌ No | ❌ No | ❌ No | ❌ No | ❌ No |
683
824
  | Column name checking | ✅ Yes | ⚠️ Limited | ❌ No | ❌ No | ❌ No | ❌ No | ❌ No | ❌ No | ❌ No | ❌ No | ❌ No |
684
825
  | Column type checking | ✅ Yes | ⚠️ Limited | ❌ No | ❌ No | ❌ No | ❌ No | ❌ No | ❌ No | ❌ No | ❌ No | ❌ No |
685
826
  | Typo suggestions | ✅ Yes | ❌ No | ❌ No | ❌ No | ❌ No | ❌ No | ❌ No | ❌ No | ❌ No | ❌ No | ❌ No |
@@ -693,7 +834,7 @@ Comprehensive comparison of pandas/DataFrame typing and validation tools. **type
693
834
  | Pandas | ✅ Yes | ✅ Yes | ✅ Yes | ✅ Yes | ✅ Yes | ✅ Yes | ✅ Yes | ❌ Own | ✅ Yes | ❌ No | ⚠️ Limited |
694
835
  | Polars | ✅ Yes | ✅ Yes | ❌ No | ❌ No | ❌ No | ❌ No | ❌ No | ❌ Own | ✅ Yes | ✅ Yes (only) | ✅ Yes |
695
836
  | DuckDB, cuDF, etc. | ❌ No | ❌ No | ✅ Spark, SQL | ❌ No | ❌ No | ❌ No | ❌ No | ❌ No | ✅ Yes | ❌ No | ❌ No |
696
- | **Project Status (Feb 2026)** |
837
+ | **Project Status (Aug 2026)** |
697
838
  | Active development | ✅ Yes | ✅ Yes | ✅ Yes | ⚠️ Low | ✅ Yes | ❌ Inactive | ⚠️ Low | ✅ Yes | ✅ Yes | ✅ Yes | ✅ Yes |
698
839
 
699
840
  **Legend:** ✅ Full support | ⚠️ Limited/Partial | ❌ Not supported
@@ -708,11 +849,13 @@ Comprehensive comparison of pandas/DataFrame typing and validation tools. **type
708
849
  hints for runtime validation via typeguard. Despite documentation implying mypy support, there is no mypy plugin —
709
850
  column access errors are not caught statically. No standalone checker. No polars support.
710
851
 
711
- - **[pandas-stubs](https://github.com/pandas-dev/pandas-stubs)** (v3.0.3): Official pandas type stubs. Provides
852
+ - **[pandas-stubs](https://github.com/pandas-dev/pandas-stubs)** (v3.0.5): Official pandas type stubs. Provides
712
853
  API-level types but no column-level checking.
713
854
 
714
- - **[dataenforce](https://github.com/CedricFR/dataenforce)** (v0.1.2): Runtime validation via decorator. Marked as
715
- experimental/not production-ready. Appears inactive. Broken on Python 3.13+ due to removal of internal typing APIs.
855
+ - **[dataenforce](https://github.com/CedricFR/dataenforce)** (v0.1.2, the only release ever published): Runtime
856
+ validation via decorator. Appears inactive/abandoned. Broken on every currently-supported Python version
857
+ (3.11 through 3.14) due to removal of internal typing APIs (`typing._TypingEmpty`) it depends on — confirmed
858
+ working only as far back as Python 3.9.
716
859
 
717
860
  - **[pandas-type-checks](https://pypi.org/project/pandas-type-checks/)** (v1.1.3): Runtime validation decorator. No
718
861
  static analysis.
@@ -721,43 +864,44 @@ Comprehensive comparison of pandas/DataFrame typing and validation tools. **type
721
864
  Not compatible with pandas/polars — requires a full rewrite to StaticFrame's own API. Column access is still
722
865
  string-based; mypy does not catch column name typos. Type safety comes from immutability guarantees, not schema checking.
723
866
 
724
- - **[narwhals](https://narwhals-dev.github.io/narwhals/)** (v2.23.0): Compatibility layer that provides a unified API
867
+ - **[narwhals](https://narwhals-dev.github.io/narwhals/)** (v2.24.0): Compatibility layer that provides a unified API
725
868
  across pandas, polars, DuckDB, cuDF, and more. Solves a different problem—write-once-run-anywhere portability, not
726
869
  type safety. See [Why Abstraction Layers Don't Solve Type Safety](#why-abstraction-layers-dont-solve-type-safety)
727
870
  below.
728
871
 
729
- - **[Great Expectations](https://greatexpectations.io/)** (v1.18.2): Comprehensive data quality framework. Defines
872
+ - **[Great Expectations](https://greatexpectations.io/)** (v1.19.1): Comprehensive data quality framework. Defines
730
873
  "expectations" (assertions) about data values, distributions, and schema properties. Excellent for runtime
731
874
  validation, data documentation, and data quality monitoring. No static analysis or column-level type checking in
732
875
  code. Supports pandas, Spark, and SQL backends.
733
876
 
734
- - **[dataframely](https://github.com/Quantco/dataframely)** (v2.13.0): Polars-only runtime validation library from Quantco.
877
+ - **[dataframely](https://github.com/Quantco/dataframely)** (v3.0.0): Polars-only runtime validation library from Quantco.
735
878
  Schemas are defined as classes inheriting `dy.Schema` with typed descriptor fields (`dy.String()`, `dy.Float64()`)
736
879
  and `@dy.rule()` decorators for cross-column and group-level constraints. Returns `dy.DataFrame[Schema]` generic
737
880
  types that give call-site narrowing to type checkers, but does not validate column subscript access inside function
738
- bodies. No lint-time or static analysis capability. Supports nullability, string constraints, numeric bounds,
739
- cross-column rules, soft validation, test data generation, and SQLAlchemy/PyArrow export.
881
+ bodies, and (as of 3.0) that narrowing doesn't even survive a `.filter()` call — it returns a plain `pl.DataFrame`.
882
+ 3.0 also removed the `dy.Series` type entirely; column access now returns a plain `pl.Series`. No lint-time or
883
+ static analysis capability. Supports nullability, string constraints, numeric bounds, cross-column rules, soft
884
+ validation, test data generation, and SQLAlchemy/PyArrow export.
740
885
 
741
886
  - **[patito](https://github.com/JakobGM/patito)** (v0.8.6): Runtime validation library using a Pydantic-style `patito.Model`
742
887
  class. Polars is the primary backend; pandas is supported but works by converting to Polars via PyArrow (an
743
- undeclared dependency). DuckDB is not supported despite appearing in some documentation—validation crashes immediately
744
- on DuckDB relations. No static analysis or standalone checker.
888
+ undeclared dependency). No static analysis or standalone checker.
745
889
 
746
890
  ### Type Checkers (Not DataFrame-Specific)
747
891
 
748
892
  These are general Python type checkers. They don't validate DataFrame column names, but they can be used alongside
749
893
  typedframes for comprehensive type checking:
750
894
 
751
- - **[mypy](https://mypy-lang.org/)** (v1.19.1): The original Python type checker. typedframes provides a mypy plugin for
895
+ - **[mypy](https://mypy-lang.org/)** (v2.3.0): The original Python type checker. typedframes provides a mypy plugin for
752
896
  column checking. See [performance benchmarks](#static-analysis-performance).
753
897
 
754
- - **[ty](https://github.com/astral-sh/ty)** (v0.0.19, Astral): New Rust-based type checker, 10-60x faster than mypy on
898
+ - **[ty](https://github.com/astral-sh/ty)** (v0.0.69, Astral): New Rust-based type checker, faster than mypy on
755
899
  large codebases. Does not support mypy plugins—use typedframes standalone checker.
756
900
 
757
- - **[pyrefly](https://pyrefly.org/)** (v0.54.0, Meta): Rust-based type checker from Meta, replacement for Pyre. Fast,
901
+ - **[pyrefly](https://pyrefly.org/)** (v1.2.0, Meta): Rust-based type checker from Meta, replacement for Pyre. Fast,
758
902
  but no DataFrame column checking.
759
903
 
760
- - **[pyright](https://github.com/microsoft/pyright)** (v1.1.408, Microsoft): Type checker powering Pylance/VSCode. No
904
+ - **[pyright](https://github.com/microsoft/pyright)** (v1.1.411, Microsoft): Type checker powering Pylance/VSCode. No
761
905
  mypy plugin support—use typedframes standalone checker.
762
906
 
763
907
  ### Not Directly Comparable
@@ -799,9 +943,9 @@ import pandas as pd
799
943
 
800
944
 
801
945
  class UserData(BaseSchema):
802
- user_id = Column(type=int)
803
- email = Column(type=str)
804
- age = Column(type=int, nullable=True)
946
+ user_id = Column(type=int)
947
+ email = Column(type=str)
948
+ age = Column(type=int, nullable=True)
805
949
 
806
950
 
807
951
  # Convert to pandera schema
@@ -839,7 +983,7 @@ class Orders(BaseSchema):
839
983
 
840
984
 
841
985
  def calculate_revenue(orders: Annotated[pd.DataFrame, Orders]) -> float:
842
- return orders['total'].sum()
986
+ return orders["total"].sum()
843
987
 
844
988
 
845
989
  df: Annotated[pd.DataFrame, Orders] = pd.read_csv("orders.csv")
@@ -891,11 +1035,17 @@ class AggregatedSales(BaseSchema):
891
1035
 
892
1036
 
893
1037
  def aggregate_daily(df: Annotated[pd.DataFrame, RawSales]) -> Annotated[pd.DataFrame, AggregatedSales]:
894
- result = df.groupby(RawSales.date.s).agg({
895
- RawSales.price.s: 'sum',
896
- RawSales.quantity.s: 'sum',
897
- }).reset_index()
898
- result.columns = pd.Index(['date', 'total_revenue', 'total_quantity'])
1038
+ result = (
1039
+ df.groupby(RawSales.date.s)
1040
+ .agg(
1041
+ {
1042
+ RawSales.price.s: "sum",
1043
+ RawSales.quantity.s: "sum",
1044
+ }
1045
+ )
1046
+ .reset_index()
1047
+ )
1048
+ result.columns = pd.Index(["date", "total_revenue", "total_quantity"])
899
1049
  return result # type: ignore[return-value]
900
1050
 
901
1051
 
@@ -906,8 +1056,8 @@ aggregated = aggregate_daily(raw)
906
1056
 
907
1057
  # Type checker validates schema transformations
908
1058
  def analyze(df: Annotated[pd.DataFrame, AggregatedSales]) -> float:
909
- df['total_revenue'] # ✓ OK
910
- df['price'] # ✗ Error: 'price' not in AggregatedSales
1059
+ df["total_revenue"] # ✓ OK
1060
+ df["price"] # ✗ Error: 'price' not in AggregatedSales
911
1061
  return df[AggregatedSales.total_revenue.s].mean()
912
1062
  ```
913
1063
 
@@ -926,11 +1076,7 @@ class LargeDataset(BaseSchema):
926
1076
 
927
1077
 
928
1078
  def efficient_aggregation(df: Annotated[pl.DataFrame, LargeDataset]) -> pl.DataFrame:
929
- return (
930
- df.filter(pl.col('value') > 100)
931
- .group_by('category')
932
- .agg(pl.col('value').mean())
933
- )
1079
+ return df.filter(pl.col("value") > 100).group_by("category").agg(pl.col("value").mean())
934
1080
 
935
1081
 
936
1082
  # Polars handles large files efficiently
@@ -982,6 +1128,7 @@ However, abstraction layers don't provide column-level type safety:
982
1128
  ```python
983
1129
  import narwhals as nw
984
1130
 
1131
+
985
1132
  def process(df: nw.DataFrame) -> nw.DataFrame:
986
1133
  # No static checking - "revenue" typo won't be caught until runtime
987
1134
  return df.filter(nw.col("revnue") > 100) # Typo: "revnue" vs "revenue"
@@ -996,11 +1143,13 @@ from typing import Annotated
996
1143
  import polars as pl
997
1144
  from typedframes import BaseSchema, Column
998
1145
 
1146
+
999
1147
  class SalesData(BaseSchema):
1000
1148
  revenue = Column(type=float)
1001
1149
 
1150
+
1002
1151
  def process(df: Annotated[pl.DataFrame, SalesData]) -> pl.DataFrame:
1003
- return df.filter(pl.col('revnue') > 100) # ✗ Error at lint-time: 'revnue' not in SalesData
1152
+ return df.filter(pl.col("revnue") > 100) # ✗ Error at lint-time: 'revnue' not in SalesData
1004
1153
  ```
1005
1154
 
1006
1155
  **Use narwhals when:** You're writing a library that needs to work with multiple DataFrame backends.
@@ -1038,6 +1187,10 @@ MIT License - see [LICENSE](LICENSE)
1038
1187
  - [x] Aggressive column inference (untracked-dataframe/dropped-unknown-column warnings, method chain propagation)
1039
1188
  - [x] Function parameter contracts (`missing-column`), resolved transitively across chains of helper
1040
1189
  functions and cross-file calls; schema-annotated parameters take priority over body-scanning
1190
+ - [x] SQL / data-warehouse column inference (`SELECT` list parsing across Snowflake, BigQuery, Athena, Redshift,
1191
+ Databricks, PySpark, DuckDB, SQLAlchemy Core/ORM, Feast, and T-SQL/Synapse/Fabric dialects)
1192
+ - [x] Jupyter notebook (`.ipynb`) checking — code cells are checked directly, with errors reported as
1193
+ `notebook.ipynb:cell N:line:col`
1041
1194
 
1042
1195
  **Planned:**
1043
1196
 
@@ -1073,7 +1226,7 @@ calls give it enough information to validate column access and propagate that kn
1073
1226
  (`rename`, `drop`, `assign`, `select`, …). `BaseSchema` is a progressive enhancement that unlocks cross-file
1074
1227
  awareness (schemas travel with function return types across module boundaries) and IDE autocomplete via
1075
1228
  descriptors — but the checker catches real column errors from day one without it. See
1076
- [`examples/multi_file_inference/`](examples/multi_file_inference/) for a complete demo with no schema classes.
1229
+ [`examples/features/multi_file_inference/`](examples/features/multi_file_inference/) for a complete demo with no schema classes.
1077
1230
 
1078
1231
  **Q: Does this work with existing pandas/polars code?**
1079
1232
  A: Yes. You can gradually adopt typedframes by adding schemas to new code. Existing code continues to work.