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.
- {typedframes-0.3.1 → typedframes-0.4.0}/PKG-INFO +249 -96
- {typedframes-0.3.1 → typedframes-0.4.0}/README.md +248 -95
- {typedframes-0.3.1 → typedframes-0.4.0}/pyproject.toml +36 -23
- typedframes-0.4.0/rust/.cargo/config.toml +21 -0
- {typedframes-0.3.1 → typedframes-0.4.0}/rust/Cargo.lock +495 -241
- {typedframes-0.3.1 → typedframes-0.4.0}/rust/Cargo.toml +18 -8
- {typedframes-0.3.1 → typedframes-0.4.0}/rust/benches/parser_bench.rs +2 -2
- typedframes-0.4.0/rust/src/ast_extract.rs +644 -0
- typedframes-0.4.0/rust/src/config.rs +199 -0
- typedframes-0.4.0/rust/src/constants.rs +248 -0
- typedframes-0.4.0/rust/src/contract.rs +200 -0
- typedframes-0.4.0/rust/src/errors.rs +103 -0
- typedframes-0.4.0/rust/src/index.rs +1715 -0
- typedframes-0.4.0/rust/src/lib.rs +57 -0
- typedframes-0.4.0/rust/src/linter.rs +6607 -0
- typedframes-0.4.0/rust/src/notebook.rs +270 -0
- typedframes-0.4.0/rust/src/pyapi.rs +251 -0
- typedframes-0.4.0/rust/src/sql.rs +774 -0
- typedframes-0.4.0/rust/src/typo.rs +90 -0
- {typedframes-0.3.1 → typedframes-0.4.0}/rust/tests/integration_test.rs +0 -62
- {typedframes-0.3.1 → typedframes-0.4.0}/src/typedframes/__init__.py +1 -1
- typedframes-0.4.0/src/typedframes/cli.py +1023 -0
- {typedframes-0.3.1 → typedframes-0.4.0}/src/typedframes/column_group.py +6 -3
- {typedframes-0.3.1 → typedframes-0.4.0}/src/typedframes/column_set.py +5 -2
- {typedframes-0.3.1 → typedframes-0.4.0}/src/typedframes/mypy.py +2 -1
- {typedframes-0.3.1 → typedframes-0.4.0}/src/typedframes/pandas.py +22 -1
- {typedframes-0.3.1 → typedframes-0.4.0}/src/typedframes/polars.py +43 -4
- typedframes-0.3.1/rust/src/lib.rs +0 -4669
- typedframes-0.3.1/src/typedframes/cli.py +0 -202
- {typedframes-0.3.1 → typedframes-0.4.0}/rust/README.md +0 -0
- {typedframes-0.3.1 → typedframes-0.4.0}/rust/src/main.rs +0 -0
- {typedframes-0.3.1 → typedframes-0.4.0}/src/typedframes/base_schema.py +0 -0
- {typedframes-0.3.1 → typedframes-0.4.0}/src/typedframes/column.py +0 -0
- {typedframes-0.3.1 → typedframes-0.4.0}/src/typedframes/column_group_error.py +0 -0
- {typedframes-0.3.1 → typedframes-0.4.0}/src/typedframes/missing_dependency_error.py +0 -0
- {typedframes-0.3.1 → typedframes-0.4.0}/src/typedframes/pandera.py +0 -0
- {typedframes-0.3.1 → typedframes-0.4.0}/src/typedframes/py.typed +0 -0
- {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
|
+
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.
|
|
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
|
-
>
|
|
47
|
+
> column checking over production-grade stability.
|
|
48
48
|
>
|
|
49
|
-
**
|
|
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
|
-
|
|
64
|
-
|
|
65
|
-
|
|
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
|
-
|
|
69
|
-
|
|
70
|
-
|
|
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
|
|
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:**
|
|
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"])
|
|
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[
|
|
179
|
-
print(df[
|
|
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())
|
|
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[
|
|
188
|
-
data[
|
|
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(
|
|
203
|
-
print(df.select(pl.col(
|
|
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(
|
|
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"])
|
|
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 (
|
|
270
|
+
**untracked-dataframe — unannotated data ingestion (on by default)**
|
|
259
271
|
|
|
260
|
-
|
|
261
|
-
|
|
262
|
-
|
|
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
|
-
|
|
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/ --
|
|
280
|
+
typedframes check src/ --lenient-ingest
|
|
268
281
|
```
|
|
269
282
|
|
|
270
|
-
|
|
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
|
|
275
|
-
# annotate
|
|
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-
|
|
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.
|
|
459
|
-
| ruff | 0.
|
|
460
|
-
| ty | 0.0.
|
|
461
|
-
| pyrefly | 1.
|
|
462
|
-
| mypy | 2.
|
|
463
|
-
| mypy + typedframes | 2.
|
|
464
|
-
| pyright | 1.1.411 | Type checker |
|
|
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[
|
|
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(
|
|
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[
|
|
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(
|
|
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()
|
|
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.
|
|
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 (
|
|
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 (
|
|
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.
|
|
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
|
|
715
|
-
|
|
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.
|
|
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.
|
|
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)** (
|
|
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.
|
|
739
|
-
|
|
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).
|
|
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/)** (
|
|
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.
|
|
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/)** (
|
|
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.
|
|
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
|
-
|
|
803
|
-
|
|
804
|
-
|
|
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[
|
|
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 =
|
|
895
|
-
RawSales.
|
|
896
|
-
|
|
897
|
-
|
|
898
|
-
|
|
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[
|
|
910
|
-
df[
|
|
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(
|
|
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.
|