typedframes 0.3.0__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.0 → typedframes-0.4.0}/PKG-INFO +265 -97
  2. {typedframes-0.3.0 → typedframes-0.4.0}/README.md +264 -96
  3. {typedframes-0.3.0 → typedframes-0.4.0}/pyproject.toml +36 -23
  4. typedframes-0.4.0/rust/.cargo/config.toml +21 -0
  5. {typedframes-0.3.0 → typedframes-0.4.0}/rust/Cargo.lock +495 -241
  6. {typedframes-0.3.0 → typedframes-0.4.0}/rust/Cargo.toml +18 -8
  7. {typedframes-0.3.0 → 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.0 → typedframes-0.4.0}/rust/tests/integration_test.rs +0 -62
  21. {typedframes-0.3.0 → 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.0 → typedframes-0.4.0}/src/typedframes/column_group.py +6 -3
  24. {typedframes-0.3.0 → typedframes-0.4.0}/src/typedframes/column_set.py +5 -2
  25. {typedframes-0.3.0 → typedframes-0.4.0}/src/typedframes/mypy.py +2 -1
  26. {typedframes-0.3.0 → typedframes-0.4.0}/src/typedframes/pandas.py +22 -1
  27. {typedframes-0.3.0 → typedframes-0.4.0}/src/typedframes/polars.py +43 -4
  28. typedframes-0.3.0/rust/src/lib.rs +0 -4285
  29. typedframes-0.3.0/src/typedframes/cli.py +0 -202
  30. {typedframes-0.3.0 → typedframes-0.4.0}/rust/README.md +0 -0
  31. {typedframes-0.3.0 → typedframes-0.4.0}/rust/src/main.rs +0 -0
  32. {typedframes-0.3.0 → typedframes-0.4.0}/src/typedframes/base_schema.py +0 -0
  33. {typedframes-0.3.0 → typedframes-0.4.0}/src/typedframes/column.py +0 -0
  34. {typedframes-0.3.0 → typedframes-0.4.0}/src/typedframes/column_group_error.py +0 -0
  35. {typedframes-0.3.0 → typedframes-0.4.0}/src/typedframes/missing_dependency_error.py +0 -0
  36. {typedframes-0.3.0 → typedframes-0.4.0}/src/typedframes/pandera.py +0 -0
  37. {typedframes-0.3.0 → typedframes-0.4.0}/src/typedframes/py.typed +0 -0
  38. {typedframes-0.3.0 → 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.0
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.0) 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
@@ -323,15 +338,44 @@ The contract is resolved *transitively*: if a function only forwards its paramet
323
338
  requirements, catching a missing column even when no single function in the chain touches it directly.
324
339
  Column-list slices (`df[["a", "b"]]`) contribute to the contract too.
325
340
 
341
+ **Known limitations:**
342
+ - Cross-file delegate/schema resolution follows `from module import name`, plain `import module` +
343
+ `module.helper(df)` attribute access, and `from module import *` wildcard imports. A dotted import with no
344
+ alias (`import a.b.c`) only binds the first segment (`a`), matching Python's own binding rules, so a
345
+ deeply nested submodule accessed without an alias is not tracked.
346
+ - Contract inference is a single top-to-bottom pass over a function body, not full control-flow analysis.
347
+ Deeply nested control flow (nested `try`/`except`, `match`, comprehensions) may under-report a function's
348
+ true requirements.
349
+ - A cycle in the delegate graph (mutually- or self-delegating helpers) contributes only each function's own
350
+ direct requirements to the cycle, not the full transitive union — conservative rather than exhaustive.
351
+ - If two plainly-imported modules both define a same-named function, an attribute-style delegate call
352
+ (`module.helper(df)`) resolves to whichever one is found first — the checker doesn't disambiguate by which
353
+ module the call site actually used.
354
+
326
355
  ### See Also
327
356
 
328
- - [`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
329
358
  scenarios with annotated ✓/✗ comments.
330
- - [`examples/multi_file_inference/`](examples/multi_file_inference/) — multi-file project checked with
331
- `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
332
361
  parameter contract violation caught at the call site (`missing-column`).
333
- - [`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`
334
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).
335
379
 
336
380
  ---
337
381
 
@@ -376,13 +420,19 @@ typedframes check src/
376
420
  typedframes check src/ --strict
377
421
 
378
422
  # JSON output
379
- typedframes check src/ --json
423
+ typedframes check src/ --output-format=json
380
424
 
381
425
  # Skip cross-file index (single-file mode, faster for quick checks)
382
426
  typedframes check src/ --no-index
383
427
 
384
428
  # Suppress all warnings (untracked-dataframe, dropped-unknown-column)
385
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
386
436
  ```
387
437
 
388
438
  To suppress warnings project-wide, add to `pyproject.toml`:
@@ -432,21 +482,126 @@ code reference.
432
482
 
433
483
  ---
434
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
+
435
589
  ## Static Analysis Performance
436
590
 
437
591
  Fast feedback reduces development time. The typedframes Rust binary provides near-instant column checking.
438
592
 
439
- **Benchmark results** (10 runs, 3 warmup, caches cleared between runs):
593
+ **Benchmark results** (20 runs, 3 warmup, caches cleared between runs):
594
+ *2026-08-12 · Darwin 25.6.0 · arm · CPython 3.14.4 · 64GiB RAM · Great Expectations pinned @ 1.9.3*
440
595
 
441
- | Tool | Version | What it does | typedframes (13 files) | great_expectations (485 files) |
596
+ | Tool | Version | What it does | typedframes (13 files) | great_expectations (482 files) |
442
597
  |------|---------|--------------|------------------------|--------------------------------|
443
- | typedframes | 0.3.0 | DataFrame column checker | 81ms ±6ms | 3.64s ±9ms |
444
- | ruff | 0.15.21 | Linter (no type checking) | 48ms ±13ms | 237ms ±31ms |
445
- | ty | 0.0.58 | Type checker | 119ms ±14ms | 287ms ±33ms |
446
- | pyrefly | 1.1.1 | Type checker | 162ms ±18ms | 390ms ±21ms |
447
- | mypy | 2.2.0 | Type checker (no plugin) | 4.56s ±22ms | 7.31s ±35ms |
448
- | mypy + typedframes | 2.2.0 | Type checker + column checker | 4.69s ±61ms | 10.89s ±356ms |
449
- | pyright | 1.1.411 | Type checker | 1.39s ±17ms | 8.06s ±193ms |
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) |
450
605
 
451
606
  *Run `uv run python benchmarks/benchmark_checkers.py` to reproduce.*
452
607
 
@@ -481,12 +636,12 @@ class UserData(BaseSchema):
481
636
 
482
637
  # Pandas pipeline - type checker knows pandas methods
483
638
  def pandas_analyze(df: Annotated[pd.DataFrame, UserData]) -> Annotated[pd.DataFrame, UserData]:
484
- return df[df['user_id'] > 100] # ✓ Pandas syntax
639
+ return df[df["user_id"] > 100] # ✓ Pandas syntax
485
640
 
486
641
 
487
642
  # Polars pipeline - type checker knows polars methods
488
643
  def polars_analyze(df: Annotated[pl.DataFrame, UserData]) -> Annotated[pl.DataFrame, UserData]:
489
- return df.filter(pl.col('user_id') > 100) # ✓ Polars syntax
644
+ return df.filter(pl.col("user_id") > 100) # ✓ Polars syntax
490
645
 
491
646
 
492
647
  # Use native types throughout
@@ -526,7 +681,7 @@ class OrderSchema(BaseSchema):
526
681
 
527
682
  # Schema preserved through filtering
528
683
  def get_active_users(df: Annotated[pd.DataFrame, UserSchema]) -> Annotated[pd.DataFrame, UserSchema]:
529
- return df[df['user_id'] > 100] # ✓ Validated by checker
684
+ return df[df["user_id"] > 100] # ✓ Validated by checker
530
685
 
531
686
 
532
687
  # Schema preserved through merges
@@ -543,7 +698,7 @@ import polars as pl
543
698
 
544
699
  # Schema columns work in filter expressions
545
700
  def filter_users(df: Annotated[pl.DataFrame, UserSchema]) -> pl.DataFrame:
546
- return df.filter(pl.col('user_id') > 100)
701
+ return df.filter(pl.col("user_id") > 100)
547
702
 
548
703
 
549
704
  # Schema columns work in join expressions
@@ -582,7 +737,7 @@ class SensorReadings(BaseSchema):
582
737
 
583
738
 
584
739
  df: Annotated[pd.DataFrame, SensorReadings] = pd.read_csv("readings.csv")
585
- 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()
586
741
  ```
587
742
 
588
743
  For logical grouping across multiple ColumnSets:
@@ -633,6 +788,7 @@ class Orders(BaseSchema):
633
788
  # Combine via multiple inheritance
634
789
  class UserOrders(UserPublic, Orders):
635
790
  """Type checkers see all columns from both parents."""
791
+
636
792
  ...
637
793
 
638
794
 
@@ -646,7 +802,7 @@ merged: Annotated[pd.DataFrame, UserOrders] = users.merge(orders, on=UserPublic.
646
802
 
647
803
  Overlapping columns with the same type are allowed (common after merges). Conflicting types raise `SchemaConflictError`.
648
804
 
649
- 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.
650
806
 
651
807
  ---
652
808
 
@@ -659,12 +815,12 @@ Comprehensive comparison of pandas/DataFrame typing and validation tools. **type
659
815
 
660
816
  | Feature | typedframes | Pandera | Great Expectations | strictly_typed_pandas | pandas-stubs | dataenforce | pandas-type-checks | StaticFrame | narwhals | dataframely | patito |
661
817
  |---------------------------------|------------------------|-------------|--------------------|-----------------------|--------------|-------------|--------------------|------------------|----------|------------------|------------------|
662
- | **Version tested** | 0.3.0 | 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 |
663
819
  | **Analysis Type** |
664
820
  | When errors are caught | **Static (lint-time)** | Runtime | Runtime | Runtime | Static | Runtime | Runtime | Runtime | Runtime | Runtime | Runtime |
665
821
  | **Static Analysis (our focus)** |
666
822
  | Mypy plugin | ✅ Yes | ⚠️ Limited | ❌ No | ❌ No | ✅ Yes | ❌ No | ❌ No | ⚠️ Basic | ❌ No | ❌ No | ❌ No |
667
- | 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 |
668
824
  | Column name checking | ✅ Yes | ⚠️ Limited | ❌ No | ❌ No | ❌ No | ❌ No | ❌ No | ❌ No | ❌ No | ❌ No | ❌ No |
669
825
  | Column type checking | ✅ Yes | ⚠️ Limited | ❌ No | ❌ No | ❌ No | ❌ No | ❌ No | ❌ No | ❌ No | ❌ No | ❌ No |
670
826
  | Typo suggestions | ✅ Yes | ❌ No | ❌ No | ❌ No | ❌ No | ❌ No | ❌ No | ❌ No | ❌ No | ❌ No | ❌ No |
@@ -678,7 +834,7 @@ Comprehensive comparison of pandas/DataFrame typing and validation tools. **type
678
834
  | Pandas | ✅ Yes | ✅ Yes | ✅ Yes | ✅ Yes | ✅ Yes | ✅ Yes | ✅ Yes | ❌ Own | ✅ Yes | ❌ No | ⚠️ Limited |
679
835
  | Polars | ✅ Yes | ✅ Yes | ❌ No | ❌ No | ❌ No | ❌ No | ❌ No | ❌ Own | ✅ Yes | ✅ Yes (only) | ✅ Yes |
680
836
  | DuckDB, cuDF, etc. | ❌ No | ❌ No | ✅ Spark, SQL | ❌ No | ❌ No | ❌ No | ❌ No | ❌ No | ✅ Yes | ❌ No | ❌ No |
681
- | **Project Status (Feb 2026)** |
837
+ | **Project Status (Aug 2026)** |
682
838
  | Active development | ✅ Yes | ✅ Yes | ✅ Yes | ⚠️ Low | ✅ Yes | ❌ Inactive | ⚠️ Low | ✅ Yes | ✅ Yes | ✅ Yes | ✅ Yes |
683
839
 
684
840
  **Legend:** ✅ Full support | ⚠️ Limited/Partial | ❌ Not supported
@@ -693,11 +849,13 @@ Comprehensive comparison of pandas/DataFrame typing and validation tools. **type
693
849
  hints for runtime validation via typeguard. Despite documentation implying mypy support, there is no mypy plugin —
694
850
  column access errors are not caught statically. No standalone checker. No polars support.
695
851
 
696
- - **[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
697
853
  API-level types but no column-level checking.
698
854
 
699
- - **[dataenforce](https://github.com/CedricFR/dataenforce)** (v0.1.2): Runtime validation via decorator. Marked as
700
- 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.
701
859
 
702
860
  - **[pandas-type-checks](https://pypi.org/project/pandas-type-checks/)** (v1.1.3): Runtime validation decorator. No
703
861
  static analysis.
@@ -706,43 +864,44 @@ Comprehensive comparison of pandas/DataFrame typing and validation tools. **type
706
864
  Not compatible with pandas/polars — requires a full rewrite to StaticFrame's own API. Column access is still
707
865
  string-based; mypy does not catch column name typos. Type safety comes from immutability guarantees, not schema checking.
708
866
 
709
- - **[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
710
868
  across pandas, polars, DuckDB, cuDF, and more. Solves a different problem—write-once-run-anywhere portability, not
711
869
  type safety. See [Why Abstraction Layers Don't Solve Type Safety](#why-abstraction-layers-dont-solve-type-safety)
712
870
  below.
713
871
 
714
- - **[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
715
873
  "expectations" (assertions) about data values, distributions, and schema properties. Excellent for runtime
716
874
  validation, data documentation, and data quality monitoring. No static analysis or column-level type checking in
717
875
  code. Supports pandas, Spark, and SQL backends.
718
876
 
719
- - **[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.
720
878
  Schemas are defined as classes inheriting `dy.Schema` with typed descriptor fields (`dy.String()`, `dy.Float64()`)
721
879
  and `@dy.rule()` decorators for cross-column and group-level constraints. Returns `dy.DataFrame[Schema]` generic
722
880
  types that give call-site narrowing to type checkers, but does not validate column subscript access inside function
723
- bodies. No lint-time or static analysis capability. Supports nullability, string constraints, numeric bounds,
724
- 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.
725
885
 
726
886
  - **[patito](https://github.com/JakobGM/patito)** (v0.8.6): Runtime validation library using a Pydantic-style `patito.Model`
727
887
  class. Polars is the primary backend; pandas is supported but works by converting to Polars via PyArrow (an
728
- undeclared dependency). DuckDB is not supported despite appearing in some documentation—validation crashes immediately
729
- on DuckDB relations. No static analysis or standalone checker.
888
+ undeclared dependency). No static analysis or standalone checker.
730
889
 
731
890
  ### Type Checkers (Not DataFrame-Specific)
732
891
 
733
892
  These are general Python type checkers. They don't validate DataFrame column names, but they can be used alongside
734
893
  typedframes for comprehensive type checking:
735
894
 
736
- - **[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
737
896
  column checking. See [performance benchmarks](#static-analysis-performance).
738
897
 
739
- - **[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
740
899
  large codebases. Does not support mypy plugins—use typedframes standalone checker.
741
900
 
742
- - **[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,
743
902
  but no DataFrame column checking.
744
903
 
745
- - **[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
746
905
  mypy plugin support—use typedframes standalone checker.
747
906
 
748
907
  ### Not Directly Comparable
@@ -784,9 +943,9 @@ import pandas as pd
784
943
 
785
944
 
786
945
  class UserData(BaseSchema):
787
- user_id = Column(type=int)
788
- email = Column(type=str)
789
- age = Column(type=int, nullable=True)
946
+ user_id = Column(type=int)
947
+ email = Column(type=str)
948
+ age = Column(type=int, nullable=True)
790
949
 
791
950
 
792
951
  # Convert to pandera schema
@@ -824,7 +983,7 @@ class Orders(BaseSchema):
824
983
 
825
984
 
826
985
  def calculate_revenue(orders: Annotated[pd.DataFrame, Orders]) -> float:
827
- return orders['total'].sum()
986
+ return orders["total"].sum()
828
987
 
829
988
 
830
989
  df: Annotated[pd.DataFrame, Orders] = pd.read_csv("orders.csv")
@@ -876,11 +1035,17 @@ class AggregatedSales(BaseSchema):
876
1035
 
877
1036
 
878
1037
  def aggregate_daily(df: Annotated[pd.DataFrame, RawSales]) -> Annotated[pd.DataFrame, AggregatedSales]:
879
- result = df.groupby(RawSales.date.s).agg({
880
- RawSales.price.s: 'sum',
881
- RawSales.quantity.s: 'sum',
882
- }).reset_index()
883
- 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"])
884
1049
  return result # type: ignore[return-value]
885
1050
 
886
1051
 
@@ -891,8 +1056,8 @@ aggregated = aggregate_daily(raw)
891
1056
 
892
1057
  # Type checker validates schema transformations
893
1058
  def analyze(df: Annotated[pd.DataFrame, AggregatedSales]) -> float:
894
- df['total_revenue'] # ✓ OK
895
- df['price'] # ✗ Error: 'price' not in AggregatedSales
1059
+ df["total_revenue"] # ✓ OK
1060
+ df["price"] # ✗ Error: 'price' not in AggregatedSales
896
1061
  return df[AggregatedSales.total_revenue.s].mean()
897
1062
  ```
898
1063
 
@@ -911,11 +1076,7 @@ class LargeDataset(BaseSchema):
911
1076
 
912
1077
 
913
1078
  def efficient_aggregation(df: Annotated[pl.DataFrame, LargeDataset]) -> pl.DataFrame:
914
- return (
915
- df.filter(pl.col('value') > 100)
916
- .group_by('category')
917
- .agg(pl.col('value').mean())
918
- )
1079
+ return df.filter(pl.col("value") > 100).group_by("category").agg(pl.col("value").mean())
919
1080
 
920
1081
 
921
1082
  # Polars handles large files efficiently
@@ -967,6 +1128,7 @@ However, abstraction layers don't provide column-level type safety:
967
1128
  ```python
968
1129
  import narwhals as nw
969
1130
 
1131
+
970
1132
  def process(df: nw.DataFrame) -> nw.DataFrame:
971
1133
  # No static checking - "revenue" typo won't be caught until runtime
972
1134
  return df.filter(nw.col("revnue") > 100) # Typo: "revnue" vs "revenue"
@@ -981,11 +1143,13 @@ from typing import Annotated
981
1143
  import polars as pl
982
1144
  from typedframes import BaseSchema, Column
983
1145
 
1146
+
984
1147
  class SalesData(BaseSchema):
985
1148
  revenue = Column(type=float)
986
1149
 
1150
+
987
1151
  def process(df: Annotated[pl.DataFrame, SalesData]) -> pl.DataFrame:
988
- 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
989
1153
  ```
990
1154
 
991
1155
  **Use narwhals when:** You're writing a library that needs to work with multiple DataFrame backends.
@@ -1023,6 +1187,10 @@ MIT License - see [LICENSE](LICENSE)
1023
1187
  - [x] Aggressive column inference (untracked-dataframe/dropped-unknown-column warnings, method chain propagation)
1024
1188
  - [x] Function parameter contracts (`missing-column`), resolved transitively across chains of helper
1025
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`
1026
1194
 
1027
1195
  **Planned:**
1028
1196
 
@@ -1058,7 +1226,7 @@ calls give it enough information to validate column access and propagate that kn
1058
1226
  (`rename`, `drop`, `assign`, `select`, …). `BaseSchema` is a progressive enhancement that unlocks cross-file
1059
1227
  awareness (schemas travel with function return types across module boundaries) and IDE autocomplete via
1060
1228
  descriptors — but the checker catches real column errors from day one without it. See
1061
- [`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.
1062
1230
 
1063
1231
  **Q: Does this work with existing pandas/polars code?**
1064
1232
  A: Yes. You can gradually adopt typedframes by adding schemas to new code. Existing code continues to work.