You signed in with another tab or window. Reload to refresh your session.You signed out in another tab or window. Reload to refresh your session.You switched accounts on another tab or window. Reload to refresh your session.Dismiss alert
Panel already depends on narwhals >=2, and Bokeh already accepts Polars and PyArrow frames as ColumnDataSource input. Panel's tabular components accept neither. I would like to close that gap, and I have done enough analysis to know where the hard part is, which is not rendering.
I am asking for agreement on three design questions before writing anything substantial.
Bokeh passes 4 of 4 non pandas cases. Panel passes 0 of 8. Identical on the released 1.8.9, so this is not a recent regression.
PyArrow fails in exactly the same places as Polars, which suggests these are general pandas API assumptions rather than anything Polars specific.
Why this seems worth doing now
Bokeh completed its side in bokeh#13780 (closed as completed, February 2025). ColumnDataSource._data_from_df already imports narwhals.stable.v1 and branches on is_pandas_like_dataframe, so the serialization layer beneath Panel is already backend neutral.
narwhals >=2 is already a hard dependency in pyproject.toml, currently used in one file, panel/pane/vega.py.
The roadmap has a section on adopting Narwhals as the DataFrame compatibility layer.
Counting pandas specific operations (.index, .iloc, .loc[, reset_index, MultiIndex, .dtypes), excluding comments and docstrings:
Module
Count
panel/widgets/tables.py
87
panel/pane/perspective.py
22
panel/reactive.py
16
The main finding: read and write need different solutions
Narwhals covers the read path completely. I checked the operations the filter and sort code actually needs, and all of them behave identically on pandas, Polars and PyArrow under narwhals.stable.v2: equality, is_in, range comparisons, str.contains case insensitive, starts_with, ends_with, to_lowercase, multi key descending sort, nulls_last, with_row_index, slicing, schema, and fill_null.
Narwhals does not help with the write path, because it has no mutation API. It is a read and transform layer, and nw.DataFrame exposes no equivalent of .loc[...] = .... Panel's editing model mutates self.value in place, so backend parity for editing needs a small explicit dispatch rather than more Narwhals.
The backends also are not equally writable:
pl.DataFrame({'a': [1, 2, 3]})[0, 'a'] =99# workspa.table({'a': [1, 2, 3]}).column('a')[0] =99# TypeError, ChunkedArray does not support item assignment
So I think the honest shape is a capability table rather than uniform parity:
Backend
Render
Filter / sort
Positional write
Label write
pandas
yes
yes
yes
yes, unchanged
polars
yes
yes
yes
not applicable, no index
pyarrow
yes
yes
no, immutable
not applicable
Proposed approach
Two internal modules, kept separate because they have different natures:
Nothing else in Panel would touch pandas directly. panel/pane/vega.py already does something similar on a smaller scale, so I would follow its idioms.
to_cds should mostly be a delegation. Since Bokeh's from_df already handles these backends, Panel's job is to stop doing its own index flattening first, not to write a new serializer.
One compatibility rule I want to state explicitly
BaseTable.add_filter() accepts a user supplied callable that receives the DataFrame and returns either a filtered frame or a boolean mask (tables.py:516-518, type(res) is type(df)). Those callables are user code that expects a concrete frame.
My proposal is that user callables always receive the native frame of whatever backend the user supplied, never a Narwhals wrapper. A pandas user's callable keeps receiving pandas. I would add a test asserting this so it cannot regress.
Questions
Is panel.widgets the right place for this? The roadmap deprecates panel.widgets, panel.pane and panel.layout at 3.0 in favour of the Material UI components. If backend parity should be built in panel-material-ui instead, or in whatever the panel.ui table becomes, I would rather know before starting. This is my main question.
What should PyArrow editing do? Tables are immutable, so the options are to raise for edits, or to reassign .value with a new frame. The second preserves parity but changes .value identity on every edit, which existing watchers may not expect.
Does the staging below look right, with the read path landing on its own first so it is useful even if the write half takes longer?
Proposed staging
Read path first, and it is a complete deliverable on its own:
panel/_dataframe.py plus a backend parametrised test matrix, non pandas cases xfail(strict=True). No behaviour change.
Index abstraction in the table base class. No behaviour change.
Column definitions and dtype mapping via Narwhals schema. No behaviour change.
Serialization routed through one function. No behaviour change.
Filter, using Narwhals expressions.
Sort and pagination slicing.
Perspective, pane.DataFrame, docs. Read parity complete, Support Polars #7812 closable.
Then the write path, which is where the risk is:
Mutation dispatch, _update_column, _map_indexes.
stream() and patch().
Steps 1 to 4 are pure refactors, which should make them fast to review. Each step leaves main green for pandas users and is independently revertible.
On sort specifically: _sort_df is a reimplementation rather than a translation, since it currently sorts with kind='mergesort' and a key= callable that lowercases string columns to match Tabulator's client side ordering, and Narwhals has no sort(key=). I intend to match the current pandas output exactly, including case handling, null placement and tie breaking. If I cannot, I will report the deviation rather than ship approximate ordering.
param.DataFrame. param#975 is not a blocker here, because BaseTable.value is a plain param.Parameter (tables.py:157), so Panel never routes through that validation.
Performance. There is no speed claim here. ColumnDataSource.from_df is about 19 ms for 500k rows and Tabulator's paginated path stays under a millisecond regardless of row count, so serialization is not a bottleneck. The case is correctness and ecosystem reach.
I have a reproduction script and the measurements behind all of the above and can share them if useful.
Summary
Panel already depends on
narwhals >=2, and Bokeh already accepts Polars and PyArrow frames asColumnDataSourceinput. Panel's tabular components accept neither. I would like to close that gap, and I have done enough analysis to know where the hard part is, which is not rendering.I am asking for agreement on three design questions before writing anything substantial.
Current state
Run against
maintoday:ColumnDataSource.from_dfwidgets.Tabulatorwidgets/tables.py:757widgets.DataFramewidgets/tables.py:757pane.DataFramepane/base.py:211pane.Perspectivepane/perspective.py:102Bokeh passes 4 of 4 non pandas cases. Panel passes 0 of 8. Identical on the released 1.8.9, so this is not a recent regression.
PyArrow fails in exactly the same places as Polars, which suggests these are general pandas API assumptions rather than anything Polars specific.
Why this seems worth doing now
ColumnDataSource._data_from_dfalready importsnarwhals.stable.v1and branches onis_pandas_like_dataframe, so the serialization layer beneath Panel is already backend neutral.narwhals >=2is already a hard dependency inpyproject.toml, currently used in one file,panel/pane/vega.py.Scope of the coupling
Counting pandas specific operations (
.index,.iloc,.loc[,reset_index,MultiIndex,.dtypes), excluding comments and docstrings:panel/widgets/tables.pypanel/pane/perspective.pypanel/reactive.pyThe main finding: read and write need different solutions
Narwhals covers the read path completely. I checked the operations the filter and sort code actually needs, and all of them behave identically on pandas, Polars and PyArrow under
narwhals.stable.v2: equality,is_in, range comparisons,str.containscase insensitive,starts_with,ends_with,to_lowercase, multi key descending sort,nulls_last,with_row_index, slicing, schema, andfill_null.Narwhals does not help with the write path, because it has no mutation API. It is a read and transform layer, and
nw.DataFrameexposes no equivalent of.loc[...] = .... Panel's editing model mutatesself.valuein place, so backend parity for editing needs a small explicit dispatch rather than more Narwhals.The backends also are not equally writable:
So I think the honest shape is a capability table rather than uniform parity:
Proposed approach
Two internal modules, kept separate because they have different natures:
Nothing else in Panel would touch pandas directly.
panel/pane/vega.pyalready does something similar on a smaller scale, so I would follow its idioms.to_cdsshould mostly be a delegation. Since Bokeh'sfrom_dfalready handles these backends, Panel's job is to stop doing its own index flattening first, not to write a new serializer.One compatibility rule I want to state explicitly
BaseTable.add_filter()accepts a user supplied callable that receives the DataFrame and returns either a filtered frame or a boolean mask (tables.py:516-518,type(res) is type(df)). Those callables are user code that expects a concrete frame.My proposal is that user callables always receive the native frame of whatever backend the user supplied, never a Narwhals wrapper. A pandas user's callable keeps receiving pandas. I would add a test asserting this so it cannot regress.
Questions
Is
panel.widgetsthe right place for this? The roadmap deprecatespanel.widgets,panel.paneandpanel.layoutat 3.0 in favour of the Material UI components. If backend parity should be built inpanel-material-uiinstead, or in whatever thepanel.uitable becomes, I would rather know before starting. This is my main question.What should PyArrow editing do? Tables are immutable, so the options are to raise for edits, or to reassign
.valuewith a new frame. The second preserves parity but changes.valueidentity on every edit, which existing watchers may not expect.Does the staging below look right, with the read path landing on its own first so it is useful even if the write half takes longer?
Proposed staging
Read path first, and it is a complete deliverable on its own:
panel/_dataframe.pyplus a backend parametrised test matrix, non pandas casesxfail(strict=True). No behaviour change.pane.DataFrame, docs. Read parity complete, Support Polars #7812 closable.Then the write path, which is where the risk is:
_update_column,_map_indexes.stream()andpatch().Steps 1 to 4 are pure refactors, which should make them fast to review. Each step leaves
maingreen for pandas users and is independently revertible.On sort specifically:
_sort_dfis a reimplementation rather than a translation, since it currently sorts withkind='mergesort'and akey=callable that lowercases string columns to match Tabulator's client side ordering, and Narwhals has nosort(key=). I intend to match the current pandas output exactly, including case handling, null placement and tie breaking. If I cannot, I will report the deviation rather than ship approximate ordering.Out of scope
pandas >=1.2would stay exactly where it is. This is only about accepting other frame types.param.DataFrame. param#975 is not a blocker here, becauseBaseTable.valueis a plainparam.Parameter(tables.py:157), so Panel never routes through that validation.ColumnDataSource.from_dfis about 19 ms for 500k rows and Tabulator's paginated path stays under a millisecond regardless of row count, so serialization is not a bottleneck. The case is correctness and ecosystem reach.I have a reproduction script and the measurements behind all of the above and can share them if useful.