feat: declare derived columns on the column itself - #2517
Open
cosmicbboy wants to merge 1 commit into
Open
cosmicbboy wants to merge 1 commit into
cosmicbboy wants to merge 1 commit into
Conversation
Adds `ParsedColumn` / `ParsedField`, so the provenance of a derived column
lives next to the column rather than in a schema-level parsers list:
class Tickets(pa.DataFrameModel):
body: str
n_words: int = pa.ParsedField(
source="body",
parser=lambda s: s.str.split().str.len(),
ge=1,
)
`ParsedColumn` is a `Column` subclass and `ParsedField` a `FieldInfo`
subclass, so everything `Column`/`Field` accepts keeps working; each
desugars into the `Parser(source=, target=)` primitive from the previous
commit. `Config.parser_source` (and `DataFrameSchema(parser_source=...)`)
is the schema-wide default for columns that do not name their own source.
Deriving from a column the schema does not declare is a `SchemaInitError`.
`@pa.parser` is generalized rather than duplicated: given a `source`, the
named fields become what it *produces* and the parser is attached to the
schema instead of the column, since a column-level parser cannot create
its own column. Without `source` it keeps its original meaning.
`parser=` also accepts an object implementing the new `ColumnParser`
protocol -- `bind(ctx)` and `batch_key(ctx)`. `bind` is called once at
schema-build time with a `ParseContext` describing the target column, so
a parser can derive its behavior from the column's declared type rather
than being told what it is producing, and can refuse with
`SchemaInitError` before any data is touched. `batch_key` groups columns
whose parsers can share work: equal non-None keys are handed to the
class's `batch` classmethod and filled by a single call, which is what
lets a schema declare one derivation per column without paying for one
pass per column.
Two details worth knowing:
- Batched parsers always receive a DataFrame (`Parser(frame_input=True)`).
Without this, a batch that happens to fill one column would take the
single-source/single-target `Series -> Series` shortcut and break.
- `ParsedColumn` needs its own backend registration: the registry keys on
the exact schema class, not the MRO, so a `Column` subclass does not
inherit `Column`'s backend.
Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Signed-off-by: Niels Bantilan <niels.bantilan@gmail.com>
cosmicbboy
force-pushed
the
feat/parser-source-target
branch
from
September 22, 2026 14:01
4f8cca9 to
722e0ef
Compare
cosmicbboy
force-pushed
the
feat/parsed-field-column
branch
from
September 22, 2026 14:01
6b1b03d to
3acc06e
Compare
This branch has not been deployed
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
Stack: 3/6 — stacked on #2516. Completes layer 1 of
specs/derived-columns-and-system-one.md(§3.3, §3.5, §3.7).Puts the provenance of a derived column next to the column, instead of in a schema-level
parserslist:ParsedColumnis aColumnsubclass andParsedFieldaFieldInfosubclass, so everything they accept keeps working — checks, coercion, nullability, description. Each desugars into theParser(source=, target=)primitive from #2516.Config.parser_source(andDataFrameSchema(parser_source=...)) is the schema-wide default for columns that don't name their own source. Deriving from a column the schema doesn't declare is aSchemaInitErrorat build time, not aKeyErrorat parse time.@pa.parsergeneralized, not duplicatedGiven a
source, the named fields become what it produces, and the parser attaches to the schema rather than the column — a column-level parser can't create its own column:Without
sourceit keeps its original meaning. One decorator, one concept.The
ColumnParserprotocolparser=also accepts an object implementing:bindis called once at schema-build time with aParseContextdescribing the target column — name, declared dtype, description, nullability, checks, resolved sources, and the schema. Two things follow, and they're the reason the protocol exists at all:system_one.Choice()derive its options from the column'sEnumwithout repeating them.SchemaInitErrorfrombindsurfaces before any data is touched.batch_keygroups columns whose parsers can share work. Equal non-Nonekeys go to the class'sbatchclassmethod and are filled by a single call;Noneopts out. This is what lets a schema declare one derivation per column without paying for one pass per column — the mechanism the System One layer needs so per-column declarations cost one request, not N.Two details worth a look
Parser(frame_input=True)). Without it, a batch that happens to fill a single column would take the single-source/single-targetSeries -> Seriesshortcut and break. Found by a test, not by inspection.ParsedColumnneeds its own backend registration. The registry keys on the exact schema class rather than walking the MRO, so aColumnsubclass doesn't inheritColumn's backend.ColumnParserinstance handed toParsedColumnis not the object that gets bound. The tests assert againstschema.columns[...].parser, and stateful parsers can't rely on the caller's reference.Testing
25 new tests in
tests/pandas/test_parsed_columns.py: object and model APIs, schema-wide default and per-column override, chained derivation declared out of order, undeclared sources, the decorator in both modes, the protocol (bind receives the right context, errors surface at build time), and batching (merged, not merged, opted out, missingbatchmethod).Suites clean: pandas+io+base (2745 passed), polars+ibis (666 passed, plus the pre-existing
test_ibis_backend_is_narwhalsfailure that also fails onmain).🤖 Generated with Claude Code