Effectively use Narwhals to write dataframe-agnostic code that works seamlessly across multiple Python dataframe libraries. Write correct type annotations for code using Narwhals.
Narwhals is a lightweight, zero-dependency compatibility layer for dataframe libraries in Python that provides a unified interface across different backends.
Docs: https://narwhals-dev.github.io/narwhals/
Narwhals enables writing dataframe-agnostic code that works seamlessly across multiple Python dataframe libraries:
Full API Support:
Lazy-Only Support:
Why Narwhals?
Target Use Case: Anyone building libraries, applications, or services that consume dataframes and need complete backend independence.
import narwhals as nw
# 1. Convert to Narwhals
df_nw = nw.from_native(df) # Works with pandas, Polars, PyArrow, etc.
# 2. Perform operations using Polars-like API
result = df_nw.select(a_sum=nw.col("a").sum(), a_mean=nw.col("a").mean(), b_std=nw.col("b").std())
# 3. Convert back to original library
result_native = result.to_native()
Simplifies function definitions for automatic conversion:
@nw.narwhalify
def my_func(df: IntoDataFrameT):
return df.select(nw.col("a").sum(), nw.col("b").mean()).filter(nw.col("a") > 0)
# Automatically handles conversion to/from Narwhals
result = my_func(pandas_df) # Works!
result = my_func(polars_df) # Also works!
from_native(df, ...): Convert native DataFrame/Series to Narwhals object
pass_through, backend, eager_only, allow_seriesto_native(nw_obj): Convert Narwhals object back to native library typenarwhalify(): Decorator for automatic dataframe-agnostic functionsnew_series(name, values, dtype): Create a new Seriesfrom_dict(data): Create DataFrame from dictionaryfrom_dicts(data): Create DataFrame from sequence of dictionariesEager Loading:
read_csv(source, **kwargs): Read CSV file into DataFrameread_parquet(source, **kwargs): Read Parquet file into DataFrameLazy Loading:
scan_csv(source, **kwargs): Lazily scan CSV filescan_parquet(source, **kwargs): Lazily scan Parquet filesum(), mean(), min(), max(), median()sum_horizontal(), mean_horizontal(), etc.col(name): Reference column by namelit(value): Create literal expressionwhen(condition): Create conditional expressionformat(template, *args): Format expression as stringgenerate_temporary_column_name(): Generate unique column namesget_native_namespace(obj): Get the native library of an objectshow_versions(): Print debugging informationcolumns: List of column namesschema: Ordered mapping of column names to dtypesshape: Tuple of (rows, columns)implementation: Name of native implementationselect(*exprs): Select columns using expressionswith_columns(*exprs): Add or modify columnsdrop(*columns): Remove specified columnsrename(mapping): Rename columnsfilter(predicate): Filter rows based on conditionshead(n): Get first n rowstail(n): Get last n rowssample(n): Randomly sample n rowsdrop_nulls(): Drop rows with null valuesunique(): Remove duplicate rowsis_empty(): Check if DataFrame has no rowsis_duplicated(): Identify duplicated rowsis_unique(): Identify unique rowsnull_count(): Count null values per columnestimated_size(): Estimate memory usagesort(*by): Sort by one or more columnsgroup_by(*by): Group by columns for aggregationjoin(other, on, how): Perform SQL-style joinspivot(on, index, values): Create pivot tableexplode(*columns): Expand list columns to long formatlazy(): Convert to LazyFrameto_native(): Convert to original library typeto_numpy(): Convert to NumPy arrayto_pandas(): Convert to pandas DataFrameto_polars(): Convert to Polars DataFrameclone(): Create a copyLazyFrame provides the same API as DataFrame but with lazy evaluation:
collect(): Materialize the LazyFrame into a DataFramecollect_schema(): Get schema without collecting datasink_parquet(path): Write results directly to ParquetAll DataFrame methods are available on LazyFrame:
select(), filter(), with_columns(), drop()group_by(), join(), sort(), unique()head(), tail(), top_k()gather_every(): Select rows at regular intervalsunpivot(): Convert from wide to long formatwith_row_index(): Add row index columnpipe(): Apply function to LazyFrameExpressions are the building blocks for column operations.
nw.col("column_name") # Reference column
nw.lit(42) # Literal value
filter(predicate): Filter elementsis_in(values): Check membershipis_between(lower, upper): Check rangedrop_nulls(): Remove nullscount(): Count non-null elementsnull_count(): Count null valuesn_unique(): Count unique valuessum(), mean(), median(): Statistical aggregationsmin(), max(): Extremesstd(), var(): Spread measuresquantile(q): Quantile valuesMathematical:
abs(): Absolute valueround(), floor(), ceil(): Roundingsqrt(), log(), exp(): Mathematical functionsType/Value Operations:
cast(dtype): Change data typefill_null(value): Replace null valuesreplace_strict(old, new): Replace specific valuesWindow Operations:
rolling_mean(window_size): Moving averagerolling_sum(window_size): Moving sumrolling_std(window_size): Moving standard deviationshift(n): Shift values by n positionsover(*by): Compute expression over groupsRanking/Uniqueness:
rank(): Assign ranksunique(): Get unique valuesis_duplicated(): Identify duplicatesis_first_distinct(): Mark first distinct occurrencesExpressions have specialized namespaces for specific data types:
String Operations (Expr.str)
DateTime Operations (Expr.dt)
List Operations (Expr.list)
Categorical Operations (Expr.cat)
Struct Operations (Expr.struct)
Name Operations (Expr.name)
Series represents a single column:
shape, dtype, namestr, dt, list, cat, structFull docs: narwhals.typing
TLDR:
DataFrameT module-attribute
DataFrameT = TypeVar('DataFrameT', bound='DataFrame[Any]') TypeVar bound to Narwhals DataFrame.
Use this if your function can accept a Narwhals DataFrame and returns a Narwhals DataFrame backed by the same backend.
Examples:
>>> import narwhals as nw
>>> from narwhals.typing import DataFrameT
>>> @nw.narwhalify
>>> def func(df: DataFrameT) -> DataFrameT:
... return df.with_columns(c=df["a"] + 1)
Frame module-attribute
Frame: TypeAlias = Union["DataFrame[Any]", "LazyFrame[Any]"] Narwhals DataFrame or Narwhals LazyFrame.
Use this if your function can work with either and your function doesn't care about its backend.
Examples:
>>> import narwhals as nw
>>> from narwhals.typing import Frame
>>> @nw.narwhalify
... def agnostic_columns(df: Frame) -> list[str]:
... return df.columns
FrameT module-attribute
FrameT = TypeVar( "FrameT", "DataFrame[Any]", "LazyFrame[Any]" ) TypeVar bound to Narwhals DataFrame or Narwhals LazyFrame.
Use this if your function accepts either nw.DataFrame or nw.LazyFrame and returns an object of the same kind.
Examples:
>>> import narwhals as nw
>>> from narwhals.typing import FrameT
>>> @nw.narwhalify
... def agnostic_func(df: FrameT) -> FrameT:
... return df.with_columns(c=nw.col("a") + 1)
IntoDataFrame module-attribute
IntoDataFrame: TypeAlias = NativeDataFrame Anything which can be converted to a Narwhals DataFrame.
Use this if your function accepts a narwhalifiable object but doesn't care about its backend.
Examples:
>>> import narwhals as nw
>>> from narwhals.typing import IntoDataFrame
>>> def agnostic_shape(df_native: IntoDataFrame) -> tuple[int, int]:
... df = nw.from_native(df_native, eager_only=True)
... return df.shape
IntoDataFrameT module-attribute
IntoDataFrameT = TypeVar( "IntoDataFrameT", bound=IntoDataFrame ) TypeVar bound to object convertible to Narwhals DataFrame.
Use this if your function accepts an object which can be converted to nw.DataFrame and returns an object of the same class.
Examples:
>>> import narwhals as nw
>>> from narwhals.typing import IntoDataFrameT
>>> def agnostic_func(df_native: IntoDataFrameT) -> IntoDataFrameT:
... df = nw.from_native(df_native, eager_only=True)
... return df.with_columns(c=df["a"] + 1).to_native()
result = df.group_by("category").agg(
count=nw.col("id").count(),
total=nw.col("amount").sum(),
average=nw.col("amount").mean(),
)
result = df.with_columns(category=nw.when(nw.col("value") > 100).then(nw.lit("high")).otherwise(nw.lit("low")))
result = df1.join(
df2,
on="key_column",
how="left", # inner, left, outer, cross
)
result = (
df.filter(nw.col("status") == "active")
.select("user_id", "amount")
.group_by("user_id")
.agg(total=nw.col("amount").sum())
.sort("total", descending=True)
.head(10)
)
Use @nw.narwhalify for library functions: Simplifies API and handles conversion automatically
Prefer expressions over method chaining: More flexible and composable
# Good
df.select(nw.col("a").sum(), nw.col("b").mean())
# Also fine, but less composable
df.select("a", "b")
Use lazy evaluation when possible: Better performance for complex pipelines
result = df.lazy().select(...).filter(...).collect()
Always convert back to native: Remember to call .to_native() when returning from library functions (unless using @narwhalify)
Type hint your functions: Use IntoDataFrame and FrameT for better IDE support
Check supported backends: Some operations may not be available on all backends
Zero Dependencies: Narwhals has no dependencies, keeping it lightweight
Polars API Subset: Uses Polars-style API but may not support all Polars features
Backend Limitations: Some backends (lazy-only) have restricted functionality
Checking whether a Narwhals frame is a Polars frame:
import polars as pl
import narwhals as nw
df_native = pl.DataFrame({"a": [1, 2, 3]})
df = nw.from_native(df_native)
df.implementation.is_polars()
Asserting series equality:
import pandas as pd
import narwhals as nw
from narwhals.testing import assert_series_equal
s1 = nw.from_native(pd.Series([1, 2, 3]), series_only=True)
s2 = nw.from_native(pd.Series([1, 5, 3]), series_only=True)
assert_series_equal(s1, s2)
Traceback (most recent call last):
...
AssertionError: Series are different (exact value mismatch)
[left]:
┌───────────────┐
|Narwhals Series|
|---------------|
| 0 1 |
| 1 2 |
| 2 3 |
| dtype: int64 |
└───────────────┘
[right]:
┌───────────────┐
|Narwhals Series|
|---------------|
| 0 1 |
| 1 5 |
| 2 3 |
| dtype: int64 |
└───────────────┘
Search for places (restaurants, cafes, etc.) via Google Places API proxy on localhost.
Interact with GitHub using the `gh` CLI. Use `gh issue`, `gh pr`, `gh run`, and `gh api` for issues, PRs, CI runs, and advanced queries.
Create or update AgentSkills. Use when designing, structuring, or packaging skills with scripts, references, and assets.
Start voice calls via the OpenClaw voice-call plugin.
Notion API for creating and managing pages, databases, and blocks.
Gemini CLI for one-shot Q&A, summaries, and generation.
Category:developer