narwhals

repository·main·Indexed 23 days ago

https://github.com/narwhals-dev/narwhals

An extremely lightweight and extensible compatibility layer between dataframe libraries. It enables developers to write dataframe-agnostic code using a subset of the Polars API that works across pandas, Polars, Modin, cuDF, PyArrow, and others. The library provides a workflow to wrap native DataFrames, transform them using Narwhals expressions, and unwrap them back to their original flavor.

Tokens
27.8K
Snippets
68
Records
146
Agent score
82%

What's inside narwhals

  1. Overview of Narwhals compatibility layer

    main

    Narwhals is an extremely lightweight and extensible compatibility layer designed to allow developers to write dataframe-agnostic code. It provides a unified interface that works across multiple dataframe libraries without requiring them as dependencies.

    Supported Libraries

    • Full API support (Eager): cuDF, Modin, pandas, Polars, PyArrow.
    • Lazy-only support: Daft, Dask, DuckDB, Ibis, PySpark, SQLFrame.

    Key Features

    • Polars-based API: Uses a subset of the Polars API, so no new syntax needs to be learned.
    • Zero dependencies: Narwhals only uses the objects passed to it by the user, ensuring your project remains lightweight.
    • Lazy and Eager separation: Provides separate APIs for lazy and eager execution using expressions.
    • Full static typing: Leverages narwhals.typing to provide IDE support and type safety.
    • Negligible overhead: Designed to be highly performant with minimal impact on execution speed.
  2. Understand the Narwhals vulnerability disclosure process

    main

    Narwhals follows a coordinated disclosure process for confirmed vulnerabilities:

    • Acknowledgment: Receipt of reports is acknowledged within 7 days.
    • Initial Assessment: An assessment (accepted, more info needed, or declined) is provided within 30 days.
    • Fix and Disclosure: For confirmed vulnerabilities, the target for a fix and coordinated disclosure is 90 days from the initial report. The reporter will be kept informed and may be asked for an embargo extension for complex issues.
    • Post-Fix: Once a fix is released, a GitHub Security Advisory is published. Reporters are credited unless they choose to remain anonymous.

    Researchers acting in good faith according to this policy will not be pursued under DMCA, CFAA, or equivalent local statutes.

  3. How Narwhals handles the pandas Index

    main

    Narwhals is designed to accommodate both pandas users who rely on Index power and those who prefer to ignore it. It follows two core principles to ensure predictable behavior:

    1. Index Preservation: Narwhals preserves your original index for most common dataframe operations. This prevents unexpected index resets. Note: Narwhals will not preserve the original index for DataFrame.group_by operations, as overlapping index and column names would cause errors.

    2. Positional Alignment (Left-hand Rule): Unlike pandas, which performs automatic index alignment (which can lead to unexpected results when sorting), Narwhals preserves the index of the left-hand-side argument. All other data is inserted positionally, similar to Polars' behavior. This avoids the

    import narwhals as nw
    import pandas as pd
    
    # Example of Index Preservation
    def my_func(df: nw.typing.IntoFrameT) -> nw.typing.IntoFrameT:
        df = nw.from_native(df)
        df = df.with_columns(a_plus_one=nw.col("a") + 1)
        return nw.to_native(df)
    
    df = pd.DataFrame({"a": [2, 1, 3], "b": [3, 5, -3]}, index=[7, 8, 9])
    print(my_func(df))  # The result still has the original index [7, 8, 9]
  4. How null preservation works with boolean columns

    main

    Narwhals operations generally preserve null values. For arithmetic operations like nw.col('a') * 2, non-null values are processed and null values remain null.

    However, for boolean comparison operations (e.g., nw.col('a') > 0), null preservation behavior depends on the underlying backend:

    • Polars and PyArrow: Null values are preserved.
    • pandas: Behavior depends on the dtype backend:
      • PyArrow dtypes or pandas nullable dtypes: Null values are preserved.
      • Classic NumPy dtypes: Null values are typically filled in with False because these dtypes do not support nulls.
    import narwhals as nw
    from narwhals.typing import IntoFrameT
    
    data = {"a": [1.4, None, 4.2]}
    
    def multiplication(df: IntoFrameT) -> IntoFrameT:
        return nw.from_native(df).with_columns((nw.col("a") * 2).alias("a*2")).to_native()
    
    # For comparison operations:
    def comparison(df: FrameT) -> FrameT:
        return nw.from_native(df).with_columns((nw.col("a") > 2).alias("a>2")).to_native()
  5. How broadcasting works in Narwhals

    main

    Narwhals performs broadcasting when comparing columns with scalars or aggregations. It treats the scalar or aggregation as if it were broadcasted to the full length of the column.

    Broadcasting is automatically triggered in these scenarios:

    1. In select: When mixing length-preserving expressions with non-length-preserving ones (e.g., df.select('a', nw.col('b').mean())).
    2. In with_columns: All new columns are broadcasted to the dataframe length.
    3. In n-ary operations: Between expressions (e.g., nw.col('a') + nw.col('a').mean()).

    Each backend implementation is responsible for the actual broadcasting logic via its CompliantExpr.broadcast method.

  6. Understand the core concept of Narwhals expressions

    main

    In Narwhals, an expression is fundamentally a function that maps a DataFrame to a sequence of Series.

    For example:

    • nw.col('a') is a function that, given a dataframe df, returns the Series 'a' from df.
    • nw.col('a') + 1 is a function that takes the Series 'a' and adds 1 to every element.
    • nw.col('a', 'b') returns a sequence containing both Series 'a' and 'b'.
    • nw.sum_horizontal('a', 'b') takes multiple columns as input and returns a single Series representing their horizontal sum.

    An expression does not produce a value by itself; it must be executed within a DataFrame context:

    • DataFrame.select: Produces a new DataFrame containing only the results of the expression(s).
    • DataFrame.with_columns: Produces a new DataFrame containing the original columns plus the results of the expression(s).
    • DataFrame.filter: Evaluates the expression (which must return a single boolean Series) and keeps only the rows where the result is True.
  7. Use the @nw.narwhalify decorator

    main

    The @nw.narwhalify decorator simplifies writing dataframe-agnostic functions by automatically handling the conversion from native formats to Narwhals and back to native formats.

    When using this decorator:

    • The input argument is treated as a FrameT (Narwhals DataFrame/LazyFrame).
    • The function should return a Narwhals object.
    • The decorator handles the nw.from_native and .to_native() calls.
    • You can pass eager_only=True to the decorator if your logic requires eager execution (e.g., accessing .shape).
    • It can also be used on functions with multiple inputs that return non-dataframe objects (like int or float).
    import narwhals as nw
    from narwhals.typing import FrameT
    
    @nw.narwhalify
    def func(df: FrameT) -> FrameT:
        return df.select(
            a_sum=nw.col("a").sum(),
            a_mean=nw.col("a").mean(),
            a_std=nw.col("a").std(),
        )
  8. Use `narwhals.LazyFrame` for deferred query execution

    main

    narwhals.LazyFrame is an abstraction used to represent a sequence of data transformations that are not executed immediately. Instead of performing computations on the data, a LazyFrame builds a logical plan of operations. This allows for optimizations and deferred execution, which is common in high-performance data processing engines.

    Key capabilities of LazyFrame include:

    • Data Transformation: Operations like filter, select, group_by, join, sort, and with_columns define the transformation pipeline.
    • Reshaping: Support for unpivot, explode, and drop to change the data structure.
    • Execution: The pipeline is eventually executed using methods like collect() (to bring data into memory) or sink_parquet() (to write directly to a file).
    • Interoperability: Use to_native() to convert the lazy representation back into the underlying engine's native format (e.g., a Polars or Pandas DataFrame).
  9. How Narwhals handles non-string and duplicate column names

    main

    Narwhals provides support for non-string column names (such as integers), which is useful when working with backends like pandas or Dask that allow them. However, there are specific constraints to keep in mind:

    1. Non-string names: While supported generally, some operations that can be ambiguous, such as DataFrame.__getitem__ or DataFrame.select, may strictly require string column names.
    2. Duplicate names: Duplicate column names are strictly prohibited (banned) in Narwhals.

    If you encounter issues using non-string column names in your specific use case, please report them to the Narwhals issue tracker.

  10. Row order constraints in LazyFrames

    main

    When working with LazyFrames, row order is undefined. Consequently, expressions used with LazyFrames must have n_orderable_ops equal to exactly zero. If an expression contains order-dependent operations, it cannot be evaluated on a LazyFrame because it depends on physical row order.

    Managing n_orderable_ops:

    • Increase: Orderable window functions like diff() or rolling_mean() increase the count by 1.
    • Decrease: Applying an over(order_by=...) immediately after an orderable window function decreases the count by 1, effectively neutralizing the order dependency for the lazy engine.
  11. How Narwhals ensures backwards compatibility

    main

    Narwhals uses a stable namespace to provide long-term stability for library maintainers. If you write code using narwhals.stable.v1 or narwhals.stable.v2, Narwhals guarantees that public functions used in those namespaces will never be changed or removed, even if the main narwhals namespace (or future versions like v3) introduces breaking changes to match updates in backend libraries like Polars.

    For example, if Polars renames cum_sum to cumulative_sum, the main Narwhals API will follow suit, but code written against narwhals.stable.v2 will continue to work with cum_sum indefinitely.

  12. How Narwhals handles lazy backends like DuckDB and PySpark

    main

    Narwhals respects the laziness of backends like DuckDB and PySpark. It will never evaluate a full query unless explicitly requested via .collect().

    To mimic Polars' behavior, Narwhals may occasionally need to inspect dataframes' schemas for specific operations. While these operations are typically cheap (often using metadata rather than reading full datasets), they do incur a small cost, particularly if data is stored in the cloud. To minimize this, Narwhals caches schema and column name evaluations.