Repository: narwhals-dev/narwhals
Pull request #2667 (2025-06-12 11:13:34)

feat: add `Expr.exp` and `Series.exp`
<!--
# Thanks for contributing a pull request! 
## Please make sure you see our contribution guidelines: https://github.com/narwhals-dev/narwhals/blob/main/CONTRIBUTING.md
-->

## What type of PR is this? (check all applicable)

- [ ] 💾 Refactor
- [x] ✨ Feature
- [ ] 🐛 Bug Fix
- [ ] 🔧 Optimization
- [ ] 📝 Documentation
- [ ] ✅ Test
- [ ] 🐳 Other

## Related issues

- Related issue #2556
- Closes #\<issue number\>

## Checklist

- [x] Code follows style guide (ruff)
- [x] Tests added
- [x] Documented the changes

## If you have comments or can explain your changes, please do so below



Patch:
diff --git a/docs/api-reference/expr.md b/docs/api-reference/expr.md
index 02e4e02a..834497a3 100644
--- a/docs/api-reference/expr.md
+++ b/docs/api-reference/expr.md
@@ -21,6 +21,7 @@
         - diff
         - drop_nulls
         - ewm_mean
+        - exp
         - fill_null
         - filter
         - gather_every
diff --git a/docs/api-reference/series.md b/docs/api-reference/series.md
index 3a49856e..abe3f3e4 100644
--- a/docs/api-reference/series.md
+++ b/docs/api-reference/series.md
@@ -26,6 +26,7 @@
         - drop_nulls
         - dtype
         - ewm_mean
+        - exp
         - fill_null
         - filter
         - gather_every
diff --git a/narwhals/_arrow/expr.py b/narwhals/_arrow/expr.py
index 605949a5..af7993c5 100644
--- a/narwhals/_arrow/expr.py
+++ b/narwhals/_arrow/expr.py
@@ -199,4 +199,7 @@ class ArrowExpr(EagerExpr["ArrowDataFrame", ArrowSeries]):
     def log(self, base: float) -> Self:
         return self._reuse_series("log", base=base)
 
+    def exp(self) -> Self:
+        return self._reuse_series("exp")
+
     ewm_mean = not_implemented()
diff --git a/narwhals/_arrow/series.py b/narwhals/_arrow/series.py
index 073bc533..8fb044b4 100644
--- a/narwhals/_arrow/series.py
+++ b/narwhals/_arrow/series.py
@@ -1156,6 +1156,9 @@ class ArrowSeries(EagerSeries["ChunkedArrayAny"]):
     def log(self, base: float) -> Self:
         return self._with_native(pc.logb(self.native, lit(base)))
 
+    def exp(self) -> Self:
+        return self._with_native(pc.exp(self.native))
+
     @property
     def dt(self) -> ArrowSeriesDateTimeNamespace:
         return ArrowSeriesDateTimeNamespace(self)
diff --git a/narwhals/_compliant/expr.py b/narwhals/_compliant/expr.py
index 9490bc8b..3435817f 100644
--- a/narwhals/_compliant/expr.py
+++ b/narwhals/_compliant/expr.py
@@ -140,6 +140,7 @@ class CompliantExpr(Protocol38[CompliantFrameT, CompliantSeriesOrNativeExprT_co]
         limit: int | None,
     ) -> Self: ...
     def diff(self) -> Self: ...
+    def exp(self) -> Self: ...
     def unique(self) -> Self: ...
     def len(self) -> Self: ...
     def log(self, base: float) -> Self: ...
diff --git a/narwhals/_compliant/series.py b/narwhals/_compliant/series.py
index fb0cb98c..93cb186c 100644
--- a/narwhals/_compliant/series.py
+++ b/narwhals/_compliant/series.py
@@ -173,6 +173,7 @@ class CompliantSeries(
         min_samples: int,
         ignore_nulls: bool,
     ) -> Self: ...
+    def exp(self) -> Self: ...
     def fill_null(
         self,
         value: Self | NonNestedLiteral,
diff --git a/narwhals/_dask/expr.py b/narwhals/_dask/expr.py
index 193c0db9..1e41c5ae 100644
--- a/narwhals/_dask/expr.py
+++ b/narwhals/_dask/expr.py
@@ -653,6 +653,11 @@ class DaskExpr(
 
         return self._with_callable(_log, "log")
 
+    def exp(self) -> Self:
+        import dask.array as da
+
+        return self._with_callable(da.exp, "exp")
+
     @property
     def str(self) -> DaskExprStringNamespace:
         return DaskExprStringNamespace(self)
diff --git a/narwhals/_duckdb/expr.py b/narwhals/_duckdb/expr.py
index f2b37742..162f9a2e 100644
--- a/narwhals/_duckdb/expr.py
+++ b/narwhals/_duckdb/expr.py
@@ -872,6 +872,12 @@ class DuckDBExpr(LazyExpr["DuckDBLazyFrame", "Expression"]):
 
         return self._with_elementwise(_log)
 
+    def exp(self) -> Self:
+        def _exp(expr: Expression) -> Expression:
+            return FunctionExpression("exp", expr)
+
+        return self._with_elementwise(_exp)
+
     @property
     def str(self) -> DuckDBExprStringNamespace:
         return DuckDBExprStringNamespace(self)
diff --git a/narwhals/_ibis/expr.py b/narwhals/_ibis/expr.py
index 5089b002..31173427 100644
--- a/narwhals/_ibis/expr.py
+++ b/narwhals/_ibis/expr.py
@@ -660,6 +660,12 @@ class IbisExpr(LazyExpr["IbisLazyFrame", "ir.Column"]):
 
         return self._with_callable(_log)
 
+    def exp(self) -> Self:
+        def _exp(expr: ir.NumericColumn) -> ir.Value:
+            return expr.exp()
+
+        return self._with_callable(_exp)
+
     @property
     def str(self) -> IbisExprStringNamespace:
         return IbisExprStringNamespace(self)
diff --git a/narwhals/_pandas_like/expr.py b/narwhals/_pandas_like/expr.py
index eb897054..0cd9958d 100644
--- a/narwhals/_pandas_like/expr.py
+++ b/narwhals/_pandas_like/expr.py
@@ -397,3 +397,6 @@ class PandasLikeExpr(EagerExpr["PandasLikeDataFrame", PandasLikeSeries]):
 
     def log(self, base: float) -> Self:
         return self._reuse_series("log", base=base)
+
+    def exp(self) -> Self:
+        return self._reuse_series("exp")
diff --git a/narwhals/_pandas_like/series.py b/narwhals/_pandas_like/series.py
index ee9af2dd..fa7f2961 100644
--- a/narwhals/_pandas_like/series.py
+++ b/narwhals/_pandas_like/series.py
@@ -1045,6 +1045,42 @@ class PandasLikeSeries(EagerSeries[Any]):
             result_native = np.log(native) / np.log(base)
         return self._with_native(result_native)
 
+    def exp(self) -> Self:
+        native = self.native
+        implementation = self._implementation
+
+        dtype_backend = get_dtype_backend(native.dtype, implementation=implementation)
+
+        if implementation.is_cudf():
+            import cupy as cp  # ignore-banned-import  # cuDF dependency.
+
+            native = self.native
+            exp_arr = cp.exp(native)
+            result_native = type(native)(exp_arr, index=native.index, name=native.name)
+            return self._with_native(result_native)
+
+        if dtype_backend == "pyarrow":
+            import pyarrow.compute as pc
+
+            from narwhals._arrow.utils import native_to_narwhals_dtype
+
+            ca = native.array._pa_array
+            result_arr = cast("ChunkedArrayAny", pc.exp(ca))
+            nw_dtype = native_to_narwhals_dtype(result_arr.type, self._version)
+            out_dtype = narwhals_to_native_dtype(
+                nw_dtype,
+                "pyarrow",
+                self._implementation,
+                self._backend_version,
+                self._version,
+            )
+            result_native = native.__class__(
+                result_arr, dtype=out_dtype, index=native.index, name=native.name
+            )
+        else:
+            result_native = np.exp(native)
+        return self._with_native(result_native)
+
     @property
     def str(self) -> PandasLikeSeriesStringNamespace:
         return PandasLikeSeriesStringNamespace(self)
diff --git a/narwhals/_polars/expr.py b/narwhals/_polars/expr.py
index f708df5a..ffd7574f 100644
--- a/narwhals/_polars/expr.py
+++ b/narwhals/_polars/expr.py
@@ -278,6 +278,7 @@ class PolarsExpr:
     cum_sum: Method[Self]
     diff: Method[Self]
     drop_nulls: Method[Self]
+    exp: Method[Self]
     fill_null: Method[Self]
     gather_every: Method[Self]
     head: Method[Self]
diff --git a/narwhals/_polars/series.py b/narwhals/_polars/series.py
index 00664a09..e4220693 100644
--- a/narwhals/_polars/series.py
+++ b/narwhals/_polars/series.py
@@ -78,6 +78,7 @@ INHERITED_METHODS = frozenset(
         "cum_sum",
         "diff",
         "drop_nulls",
+        "exp",
         "fill_null",
         "filter",
         "gather_every",
@@ -627,6 +628,7 @@ class PolarsSeries:
     cum_sum: Method[Self]
     diff: Method[Self]
     drop_nulls: Method[Self]
+    exp: Method[Self]
     fill_null: Method[Self]
     filter: Method[Self]
     gather_every: Method[Self]
diff --git a/narwhals/_spark_like/expr.py b/narwhals/_spark_like/expr.py
index 0247fa3b..2e979f2f 100644
--- a/narwhals/_spark_like/expr.py
+++ b/narwhals/_spark_like/expr.py
@@ -923,6 +923,12 @@ class SparkLikeExpr(LazyExpr["SparkLikeLazyFrame", "Column"]):
 
         return self._with_elementwise(_log)
 
+    def exp(self) -> Self:
+        def _exp(expr: Column) -> Column:
+            return self._F.exp(expr)
+
+        return self._with_elementwise(_exp)
+
     @property
     def str(self) -> SparkLikeExprStringNamespace:
         return SparkLikeExprStringNamespace(self)
diff --git a/narwhals/expr.py b/narwhals/expr.py
index f3b1817e..91de4ad2 100644
--- a/narwhals/expr.py
+++ b/narwhals/expr.py
@@ -2461,7 +2461,7 @@ class Expr:
             base: Given base, defaults to `e`
 
         Returns:
-            A new expression log values data.
+            A new expression.
 
         Examples:
             >>> import pyarrow as pa
@@ -2489,6 +2489,32 @@ class Expr:
             lambda plx: self._to_compliant_expr(plx).log(base=base)
         )
 
+    def exp(self) -> Self:
+        r"""Compute the exponent.
+
+        Returns:
+            A new expression.
+
+        Examples:
+            >>> import pyarrow as pa
+            >>> import narwhals as nw
+            >>> df_native = pa.table({"values": [-1, 0, 1]})
+            >>> df = nw.from_native(df_native)
+            >>> result = df.with_columns(exp=nw.col("values").exp())
+            >>> result
+            ┌────────────────────────────────────────────────┐
+            |               Narwhals DataFrame               |
+            |------------------------------------------------|
+            |pyarrow.Table                                   |
+            |values: int64                                   |
+            |exp: double                                     |
+            |----                                            |
+            |values: [[-1,0,1]]                              |
+            |exp: [[0.36787944117144233,1,2.718281828459045]]|
+            └────────────────────────────────────────────────┘
+        """
+        return self._with_elementwise_op(lambda plx: self._to_compliant_expr(plx).exp())
+
     @property
     def str(self) -> ExprStringNamespace[Self]:
         return ExprStringNamespace(self)
diff --git a/narwhals/series.py b/narwhals/series.py
index 73d5f137..d0cbdcf1 100644
--- a/narwhals/series.py
+++ b/narwhals/series.py
@@ -2601,7 +2601,7 @@ class Series(Generic[IntoSeriesT]):
             base: Given base, defaults to `e`
 
         Returns:
-            A new expression log values data.
+            A new series.
 
         Examples:
             >>> import pandas as pd
@@ -2620,6 +2620,29 @@ class Series(Generic[IntoSeriesT]):
         """
         return self._with_compliant(self._compliant_series.log(base=base))
 
+    def exp(self) -> Self:
+        r"""Compute the exponent.
+
+        Returns:
+            A new series.
+
+        Examples:
+            >>> import pandas as pd
+            >>> import narwhals as nw
+            >>> s_native = pd.Series([-1, 0, 1], name="a")
+            >>> s = nw.from_native(s_native, series_only=True)
+            >>> s.exp()
+            ┌───────────────────────┐
+            |    Narwhals Series    |
+            |-----------------------|
+            |0    0.367879          |
+            |1    1.000000          |
+            |2    2.718282          |
+            |Name: a, dtype: float64|
+            └───────────────────────┘
+        """
+        return self._with_compliant(self._compliant_series.exp())
+
     @property
     def str(self) -> SeriesStringNamespace[Self]:
         return SeriesStringNamespace(self)


Test patch:
diff --git a/tests/expr_and_series/exp_test.py b/tests/expr_and_series/exp_test.py
new file mode 100644
index 00000000..3cfcf1e1
--- /dev/null
+++ b/tests/expr_and_series/exp_test.py
@@ -0,0 +1,52 @@
+from __future__ import annotations
+
+import pandas as pd
+import pytest
+
+import narwhals.stable.v1 as nw
+from tests.utils import PANDAS_VERSION, Constructor, ConstructorEager, assert_equal_data
+
+data = {"a": [-1, 0, 1, 2, 4]}
+
+expected = [0.367879, 1.0, 2.718282, 7.389056, 54.59815]
+
+
+@pytest.mark.filterwarnings("ignore::RuntimeWarning")
+def test_exp_expr(constructor: Constructor) -> None:
+    df = nw.from_native(constructor(data))
+    result = df.select(nw.col("a").exp())
+    assert_equal_data(result, {"a": expected})
+
+
+@pytest.mark.filterwarnings("ignore::RuntimeWarning")
+def test_exp_series(constructor_eager: ConstructorEager) -> None:
+    series = nw.from_native(constructor_eager(data), eager_only=True)["a"]
+    result = series.exp()
+    assert_equal_data({"a": result}, {"a": expected})
+
+
+def test_exp_dtype_pandas() -> None:
+    s = pd.Series([1.0, 2.0], name="a", dtype="float32", index=[8, 7])
+    result = nw.from_native(s, series_only=True).exp().to_native()
+    expected = pd.Series([2.718282, 7.389056], name="a", dtype="float32", index=[8, 7])
+    pd.testing.assert_series_equal(result, expected)
+
+
+@pytest.mark.skipif(PANDAS_VERSION < (2, 0, 0), reason="nullable types require pandas2+")
+def test_exp_dtype_pandas_nullabe() -> None:
+    s = pd.Series([1.0, None, 2.0], name="a", dtype="Float32", index=[8, 7, 6])
+    result = nw.from_native(s, series_only=True).exp().to_native()
+    expected = pd.Series(
+        [2.718282, None, 7.389056], name="a", dtype="Float32", index=[8, 7, 6]
+    )
+    pd.testing.assert_series_equal(result, expected)
+
+
+@pytest.mark.skipif(PANDAS_VERSION < (2, 1, 0), reason="nullable types require pandas2+")
+def test_exp_dtype_pandas_pyarrow() -> None:
+    s = pd.Series([1.0, None, 2.0], name="a", dtype="Float32[pyarrow]", index=[8, 7, 6])
+    result = nw.from_native(s, series_only=True).exp().to_native()
+    expected = pd.Series(
+        [2.718282, None, 7.389056], name="a", dtype="Float32[pyarrow]", index=[8, 7, 6]
+    )
+    pd.testing.assert_series_equal(result, expected)