One DataFrame API for Pandas and Polars with Narwhals (2026) — ship library code that accepts either backend without maintaining two code paths.
You already compared engines in Polars vs Pandas and pipeline them with DuckDB + Polars. Narwhals is the thin compatibility layer: Polars-shaped expressions, native objects in and out.
TL;DR
nw.from_native(df)→ Polars-like API →.to_native()@nw.narwhalifywraps both conversions for you- Zero hard dependency on Pandas or Polars — only what the caller passes
- Use
eager_only=Truewhen you need Series /.shape
Install
pip install narwhals pandas polars
# or: uv add narwhals pandas polars
Example 1 — from_native / to_native
import narwhals as nw
import pandas as pd
import polars as pl
def summarize(df_native):
return (
nw.from_native(df_native)
.group_by("region")
.agg(nw.col("sales").sum().alias("sales_sum"))
.sort("region")
.to_native()
)
pdf = pd.DataFrame({"region": ["N", "S", "N"], "sales": [10, 7, 3]})
pldf = pl.DataFrame({"region": ["N", "S", "N"], "sales": [10, 7, 3]})
print(summarize(pdf))
print(summarize(pldf))
Example 2 — @nw.narwhalify
import narwhals as nw
@nw.narwhalify
def with_total(df):
return df.with_columns(total=nw.col("price") * nw.col("qty"))
# Pandas in → Pandas out; Polars in → Polars out
Interactive exploration still pairs well with marimo notebooks.
Production tips
- Type with
IntoFrameTso callers see agnostic signatures - Do not mix eager-only APIs into lazy paths without
eager_only=True - Keep Narwhals on the library boundary; leave domain code on one native engine when possible
- Test with both Pandas and Polars CI jobs
Wrap-up
In 2026, dataframe libraries multiply. Narwhals lets you support Pandas and Polars with one expression-style function — the practical default for reusable data utilities.