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.
- {typedframes-0.3.0 → typedframes-0.4.0}/PKG-INFO +265 -97
- {typedframes-0.3.0 → typedframes-0.4.0}/README.md +264 -96
- {typedframes-0.3.0 → typedframes-0.4.0}/pyproject.toml +36 -23
- typedframes-0.4.0/rust/.cargo/config.toml +21 -0
- {typedframes-0.3.0 → typedframes-0.4.0}/rust/Cargo.lock +495 -241
- {typedframes-0.3.0 → typedframes-0.4.0}/rust/Cargo.toml +18 -8
- {typedframes-0.3.0 → 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.0 → typedframes-0.4.0}/rust/tests/integration_test.rs +0 -62
- {typedframes-0.3.0 → typedframes-0.4.0}/src/typedframes/__init__.py +1 -1
- typedframes-0.4.0/src/typedframes/cli.py +1023 -0
- {typedframes-0.3.0 → typedframes-0.4.0}/src/typedframes/column_group.py +6 -3
- {typedframes-0.3.0 → typedframes-0.4.0}/src/typedframes/column_set.py +5 -2
- {typedframes-0.3.0 → typedframes-0.4.0}/src/typedframes/mypy.py +2 -1
- {typedframes-0.3.0 → typedframes-0.4.0}/src/typedframes/pandas.py +22 -1
- {typedframes-0.3.0 → typedframes-0.4.0}/src/typedframes/polars.py +43 -4
- typedframes-0.3.0/rust/src/lib.rs +0 -4285
- typedframes-0.3.0/src/typedframes/cli.py +0 -202
- {typedframes-0.3.0 → typedframes-0.4.0}/rust/README.md +0 -0
- {typedframes-0.3.0 → typedframes-0.4.0}/rust/src/main.rs +0 -0
- {typedframes-0.3.0 → typedframes-0.4.0}/src/typedframes/base_schema.py +0 -0
- {typedframes-0.3.0 → typedframes-0.4.0}/src/typedframes/column.py +0 -0
- {typedframes-0.3.0 → typedframes-0.4.0}/src/typedframes/column_group_error.py +0 -0
- {typedframes-0.3.0 → typedframes-0.4.0}/src/typedframes/missing_dependency_error.py +0 -0
- {typedframes-0.3.0 → typedframes-0.4.0}/src/typedframes/pandera.py +0 -0
- {typedframes-0.3.0 → typedframes-0.4.0}/src/typedframes/py.typed +0 -0
- {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
|
+
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
|
|
@@ -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** (
|
|
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 (
|
|
596
|
+
| Tool | Version | What it does | typedframes (13 files) | great_expectations (482 files) |
|
|
442
597
|
|------|---------|--------------|------------------------|--------------------------------|
|
|
443
|
-
| typedframes | 0.
|
|
444
|
-
| ruff | 0.
|
|
445
|
-
| ty | 0.0.
|
|
446
|
-
| pyrefly | 1.
|
|
447
|
-
| mypy | 2.
|
|
448
|
-
| mypy + typedframes | 2.
|
|
449
|
-
| 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) |
|
|
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[
|
|
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(
|
|
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[
|
|
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(
|
|
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()
|
|
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.
|
|
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 (
|
|
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 (
|
|
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.
|
|
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
|
|
700
|
-
|
|
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.
|
|
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.
|
|
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)** (
|
|
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.
|
|
724
|
-
|
|
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).
|
|
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/)** (
|
|
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.
|
|
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/)** (
|
|
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.
|
|
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
|
-
|
|
788
|
-
|
|
789
|
-
|
|
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[
|
|
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 =
|
|
880
|
-
RawSales.
|
|
881
|
-
|
|
882
|
-
|
|
883
|
-
|
|
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[
|
|
895
|
-
df[
|
|
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(
|
|
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.
|