Repository: narwhals-dev/narwhals
Pull request #1168 (2024-10-12 21:19:51)

feat: add The Zen of Narwhals
```
>>> import narwhals.this
⣿⣿⣿⣿⣿⠘⣿⣿⣿⣿⣿⣿⣿⣿⣿⣿⣿⣿⣿⣿⣿⣿⣿⣿⣿⣿⣿⣿⣿⣿ THE ZEN OF NARWHALS
⣿⣿⣿⣿⣿⠠⢹⣿⣿⣿⣿⣿⣿⣿⣿⣿⣿⣿⣿⣿⣿⣿⣿⣿⣿⣿⣿⣿⣿⣿ Keep it simple
⣿⣿⣿⣿⣿⡀⡄⢿⣿⣿⣿⣿⣿⣿⣿⣿⣿⣿⣿⣿⣿⣿⣿⣿⣿⣿⣿⣿⣿⣿ Move slowly and keep things working
⣿⣿⣿⣿⣿⡇⡼⡘⠛⠿⠿⠿⢿⣿⣿⣿⣿⣿⣿⣿⣿⣿⣿⣿⣿⣿⣿⣿⣿⣿ A good API is an honest one
⣿⣿⣿⡿⣫⡄⠾⣣⠹⣿⣿⣿⣶⣮⣙⠻⢿⣿⣿⣿⣿⣿⣿⣿⣿⣿⣿⣿⣿⣿ Yes, that needs documenting
⣿⣿⢋⣴⣿⣷⣬⣭⣾⣿⣿⣿⣿⣿⣿⣿⣦⡙⣿⣿⣿⣿⣿⣿⣿⣿⣿⣿⣿⣿ People learn better from examples
⣿⢃⣾⣿⣿⣿⣿⣿⣿⣿⣿⣿⣿⣿⣿⣿⣿⣷⡌⢿⣿⣿⣿⣿⣿⣿⣿⣿⣿⣿ than from explanations⠀
⡏⠀⢰⠄⢻⣿⣿⣿⣿⡿⠋⢉⠻⣿⣿⣿⣿⣿⣿⡜⣿⣿⡿⢁⢻⣿⣿⣿⣿⣿ If in doubt, say 'no'⠀
⡇⣌⣀⣠⣾⣿⣿⣿⣿⣇⠶⠉⢁⣿⣿⣿⣿⣿⣿⣧⡹⣿⡇⣿⣧⠻⠿⠿⠿⠿ you can always reconsider⠀
⡧⢹⣿⣿⣿⣜⣟⣸⣿⣿⣷⣶⣿⡿⣿⣿⣝⢿⣿⣿⣷⣬⣥⣿⣿⣿⣿⣿⡟⣰ Yes, we need a test for that
⢡⣆⢻⣿⣿⣿⣿⣿⣿⣿⣿⣿⣿⣧⡙⣿⣿⡇⣿⣿⣿⣿⠟⣋⣭⣛⠻⣋⣴⣿ If you want users
⣶⣤⣤⣙⠻⣿⣿⣿⣿⣿⣿⣿⣿⣿⣷⣦⣍⣡⣿⡿⢋⣴⣿⣿⣿⣿⣿⣿⣿⣿ you need good docs⠀
⣿⣿⣿⣿⣿⣶⣬⣙⣛⠻⠿⠿⠿⠿⠿⠟⣛⣩⣥⣶⣿⣿⣿⣿⣿⣿⣿⣿⣿⣿ Our code is not irreplaceable
```


Patch:
diff --git a/.github/workflows/bump-version.yml b/.github/workflows/bump-version.yml
new file mode 100644
index 00000000..b6360cc3
--- /dev/null
+++ b/.github/workflows/bump-version.yml
@@ -0,0 +1,47 @@
+name: Bump version
+
+on:
+  workflow_dispatch:
+    inputs:
+      release_type:     
+        description: 'Type of version bump: patch, minor, or major'
+        required: true
+        default: 'patch'
+        type: choice
+        options: 
+        - patch
+        - minor
+        - major
+
+jobs:
+  bump-version:
+    runs-on: ubuntu-latest
+    steps:
+
+    - uses: actions/checkout@v4
+
+    - uses: actions/setup-python@v5
+      with:
+        python-version: "3.11"
+
+    - name: bump version
+      run: |
+        git config user.name "github-actions[bot]"
+        git config user.email "github-actions[bot]@users.noreply.github.com"
+        git checkout -b bump-version
+        python utils/bump_version.py ${{ github.event.inputs.release_type }}
+
+    - name: Create pull request
+      uses: actions/github-script@v6
+      if: github.actor == 'MarcoGorelli' || github.actor == 'FBruzzesi'
+      with:
+        script: |
+          const branch = `bump-version`
+          github.pulls.create({
+            owner: context.repo.owner,
+            repo: context.repo.repo,
+            head: branch,
+            base: 'main',
+            body: 'Bumping the version to reflect the latest release type.',
+          })
+        
\ No newline at end of file
diff --git a/docs/api-reference/narwhals.md b/docs/api-reference/narwhals.md
index 39afb8e8..044b20e0 100644
--- a/docs/api-reference/narwhals.md
+++ b/docs/api-reference/narwhals.md
@@ -20,6 +20,7 @@ Here are the top-level functions available in Narwhals.
         - len
         - lit
         - max
+        - max_horizontal
         - maybe_align_index
         - maybe_convert_dtypes
         - maybe_get_index
@@ -28,6 +29,7 @@ Here are the top-level functions available in Narwhals.
         - mean
         - mean_horizontal
         - min
+        - min_horizontal
         - narwhalify
         - new_series
         - nth
diff --git a/narwhals/__init__.py b/narwhals/__init__.py
index 4f0b6ae8..124f10c4 100644
--- a/narwhals/__init__.py
+++ b/narwhals/__init__.py
@@ -34,9 +34,11 @@ from narwhals.expr import concat_str
 from narwhals.expr import len_ as len
 from narwhals.expr import lit
 from narwhals.expr import max
+from narwhals.expr import max_horizontal
 from narwhals.expr import mean
 from narwhals.expr import mean_horizontal
 from narwhals.expr import min
+from narwhals.expr import min_horizontal
 from narwhals.expr import nth
 from narwhals.expr import sum
 from narwhals.expr import sum_horizontal
@@ -84,10 +86,12 @@ __all__ = [
     "concat_str",
     "len",
     "lit",
-    "min",
     "max",
+    "max_horizontal",
     "mean",
     "mean_horizontal",
+    "min",
+    "min_horizontal",
     "nth",
     "sum",
     "sum_horizontal",
diff --git a/narwhals/_arrow/namespace.py b/narwhals/_arrow/namespace.py
index c514110f..5eae258f 100644
--- a/narwhals/_arrow/namespace.py
+++ b/narwhals/_arrow/namespace.py
@@ -237,6 +237,62 @@ class ArrowNamespace:
             output_names=reduce_output_names(parsed_exprs),
         )
 
+    def min_horizontal(self, *exprs: IntoArrowExpr) -> ArrowExpr:
+        import pyarrow.compute as pc  # ignore-banned-import
+
+        parsed_exprs = parse_into_exprs(*exprs, namespace=self)
+
+        def func(df: ArrowDataFrame) -> list[ArrowSeries]:
+            init_series, *series = [s for _expr in parsed_exprs for s in _expr._call(df)]
+            return [
+                ArrowSeries(
+                    native_series=reduce(
+                        lambda x, y: pc.min_element_wise(x, y),
+                        [s._native_series for s in series],
+                        init_series._native_series,
+                    ),
+                    name=init_series.name,
+                    backend_version=self._backend_version,
+                    dtypes=self._dtypes,
+                )
+            ]
+
+        return self._create_expr_from_callable(
+            func=func,
+            depth=max(x._depth for x in parsed_exprs) + 1,
+            function_name="min_horizontal",
+            root_names=combine_root_names(parsed_exprs),
+            output_names=reduce_output_names(parsed_exprs),
+        )
+
+    def max_horizontal(self, *exprs: IntoArrowExpr) -> ArrowExpr:
+        import pyarrow.compute as pc  # ignore-banned-import
+
+        parsed_exprs = parse_into_exprs(*exprs, namespace=self)
+
+        def func(df: ArrowDataFrame) -> list[ArrowSeries]:
+            init_series, *series = [s for _expr in parsed_exprs for s in _expr._call(df)]
+            return [
+                ArrowSeries(
+                    native_series=reduce(
+                        lambda x, y: pc.max_element_wise(x, y),
+                        [s._native_series for s in series],
+                        init_series._native_series,
+                    ),
+                    name=init_series.name,
+                    backend_version=self._backend_version,
+                    dtypes=self._dtypes,
+                )
+            ]
+
+        return self._create_expr_from_callable(
+            func=func,
+            depth=max(x._depth for x in parsed_exprs) + 1,
+            function_name="max_horizontal",
+            root_names=combine_root_names(parsed_exprs),
+            output_names=reduce_output_names(parsed_exprs),
+        )
+
     def concat(
         self,
         items: Iterable[ArrowDataFrame],
diff --git a/narwhals/_dask/namespace.py b/narwhals/_dask/namespace.py
index e0f2bbbd..23c133a1 100644
--- a/narwhals/_dask/namespace.py
+++ b/narwhals/_dask/namespace.py
@@ -232,7 +232,7 @@ class DaskNamespace:
             )
         raise NotImplementedError
 
-    def mean_horizontal(self, *exprs: IntoDaskExpr) -> IntoDaskExpr:
+    def mean_horizontal(self, *exprs: IntoDaskExpr) -> DaskExpr:
         parsed_exprs = parse_into_exprs(*exprs, namespace=self)
 
         def func(df: DaskLazyFrame) -> list[dask_expr.Series]:
@@ -256,6 +256,48 @@ class DaskNamespace:
             dtypes=self._dtypes,
         )
 
+    def min_horizontal(self, *exprs: IntoDaskExpr) -> DaskExpr:
+        import dask.dataframe as dd  # ignore-banned-import
+
+        parsed_exprs = parse_into_exprs(*exprs, namespace=self)
+
+        def func(df: DaskLazyFrame) -> list[dask_expr.Series]:
+            series = [s for _expr in parsed_exprs for s in _expr._call(df)]
+
+            return [dd.concat(series, axis=1).min(axis=1).rename(series[0].name)]
+
+        return DaskExpr(
+            call=func,
+            depth=max(x._depth for x in parsed_exprs) + 1,
+            function_name="min_horizontal",
+            root_names=combine_root_names(parsed_exprs),
+            output_names=reduce_output_names(parsed_exprs),
+            returns_scalar=False,
+            backend_version=self._backend_version,
+            dtypes=self._dtypes,
+        )
+
+    def max_horizontal(self, *exprs: IntoDaskExpr) -> DaskExpr:
+        import dask.dataframe as dd  # ignore-banned-import
+
+        parsed_exprs = parse_into_exprs(*exprs, namespace=self)
+
+        def func(df: DaskLazyFrame) -> list[dask_expr.Series]:
+            series = [s for _expr in parsed_exprs for s in _expr._call(df)]
+
+            return [dd.concat(series, axis=1).max(axis=1).rename(series[0].name)]
+
+        return DaskExpr(
+            call=func,
+            depth=max(x._depth for x in parsed_exprs) + 1,
+            function_name="max_horizontal",
+            root_names=combine_root_names(parsed_exprs),
+            output_names=reduce_output_names(parsed_exprs),
+            returns_scalar=False,
+            backend_version=self._backend_version,
+            dtypes=self._dtypes,
+        )
+
     def _create_expr_from_series(self, _: Any) -> NoReturn:
         msg = "`_create_expr_from_series` for DaskNamespace exists only for compatibility"
         raise NotImplementedError(msg)
diff --git a/narwhals/_pandas_like/namespace.py b/narwhals/_pandas_like/namespace.py
index 8f6e1751..63d3454a 100644
--- a/narwhals/_pandas_like/namespace.py
+++ b/narwhals/_pandas_like/namespace.py
@@ -278,6 +278,60 @@ class PandasLikeNamespace:
             output_names=reduce_output_names(parsed_exprs),
         )
 
+    def min_horizontal(self, *exprs: IntoPandasLikeExpr) -> PandasLikeExpr:
+        parsed_exprs = parse_into_exprs(*exprs, namespace=self)
+
+        def func(df: PandasLikeDataFrame) -> list[PandasLikeSeries]:
+            series = [s for _expr in parsed_exprs for s in _expr._call(df)]
+
+            return [
+                PandasLikeSeries(
+                    native_series=self.concat(
+                        (s.to_frame() for s in series), how="horizontal"
+                    )
+                    ._native_frame.min(axis=1)
+                    .rename(series[0].name),
+                    implementation=self._implementation,
+                    backend_version=self._backend_version,
+                    dtypes=self._dtypes,
+                )
+            ]
+
+        return self._create_expr_from_callable(
+            func=func,
+            depth=max(x._depth for x in parsed_exprs) + 1,
+            function_name="min_horizontal",
+            root_names=combine_root_names(parsed_exprs),
+            output_names=reduce_output_names(parsed_exprs),
+        )
+
+    def max_horizontal(self, *exprs: IntoPandasLikeExpr) -> PandasLikeExpr:
+        parsed_exprs = parse_into_exprs(*exprs, namespace=self)
+
+        def func(df: PandasLikeDataFrame) -> list[PandasLikeSeries]:
+            series = [s for _expr in parsed_exprs for s in _expr._call(df)]
+
+            return [
+                PandasLikeSeries(
+                    native_series=self.concat(
+                        (s.to_frame() for s in series), how="horizontal"
+                    )
+                    ._native_frame.max(axis=1)
+                    .rename(series[0].name),
+                    implementation=self._implementation,
+                    backend_version=self._backend_version,
+                    dtypes=self._dtypes,
+                )
+            ]
+
+        return self._create_expr_from_callable(
+            func=func,
+            depth=max(x._depth for x in parsed_exprs) + 1,
+            function_name="max_horizontal",
+            root_names=combine_root_names(parsed_exprs),
+            output_names=reduce_output_names(parsed_exprs),
+        )
+
     def concat(
         self,
         items: Iterable[PandasLikeDataFrame],
diff --git a/narwhals/expr.py b/narwhals/expr.py
index abf9c104..edd52b30 100644
--- a/narwhals/expr.py
+++ b/narwhals/expr.py
@@ -4221,6 +4221,132 @@ def sum_horizontal(*exprs: IntoExpr | Iterable[IntoExpr]) -> Expr:
     )
 
 
+def min_horizontal(*exprs: IntoExpr | Iterable[IntoExpr]) -> Expr:
+    """
+    Get the minimum value horizontally across columns.
+
+    Notes:
+        We support `min_horizontal` over numeric columns only.
+
+    Arguments:
+        exprs: Name(s) of the columns to use in the aggregation function. Accepts
+            expression input.
+
+    Examples:
+        >>> import narwhals as nw
+        >>> import pandas as pd
+        >>> import polars as pl
+        >>> import pyarrow as pa
+        >>> data = {
+        ...     "a": [1, 8, 3],
+        ...     "b": [4, 5, None],
+        ...     "c": ["x", "y", "z"],
+        ... }
+
+        We define a dataframe-agnostic function that computes the horizontal min of "a"
+        and "b" columns:
+
+        >>> @nw.narwhalify
+        ... def func(df):
+        ...     return df.select(nw.min_horizontal("a", "b"))
+
+        We can then pass either pandas, polars or pyarrow to `func`:
+
+        >>> func(pd.DataFrame(data))
+             a
+        0  1.0
+        1  5.0
+        2  3.0
+        >>> func(pl.DataFrame(data))
+        shape: (3, 1)
+        ┌─────┐
+        │ a   │
+        │ --- │
+        │ i64 │
+        ╞═════╡
+        │ 1   │
+        │ 5   │
+        │ 3   │
+        └─────┘
+        >>> func(pa.table(data))
+        pyarrow.Table
+        a: int64
+        ----
+        a: [[1,5,3]]
+    """
+    if not exprs:
+        msg = "At least one expression must be passed to `min_horizontal`"
+        raise ValueError(msg)
+    return Expr(
+        lambda plx: plx.min_horizontal(
+            *[extract_compliant(plx, v) for v in flatten(exprs)]
+        )
+    )
+
+
+def max_horizontal(*exprs: IntoExpr | Iterable[IntoExpr]) -> Expr:
+    """
+    Get the maximum value horizontally across columns.
+
+    Notes:
+        We support `max_horizontal` over numeric columns only.
+
+    Arguments:
+        exprs: Name(s) of the columns to use in the aggregation function. Accepts
+            expression input.
+
+    Examples:
+        >>> import narwhals as nw
+        >>> import pandas as pd
+        >>> import polars as pl
+        >>> import pyarrow as pa
+        >>> data = {
+        ...     "a": [1, 8, 3],
+        ...     "b": [4, 5, None],
+        ...     "c": ["x", "y", "z"],
+        ... }
+
+        We define a dataframe-agnostic function that computes the horizontal max of "a"
+        and "b" columns:
+
+        >>> @nw.narwhalify
+        ... def func(df):
+        ...     return df.select(nw.max_horizontal("a", "b"))
+
+        We can then pass either pandas, polars or pyarrow to `func`:
+
+        >>> func(pd.DataFrame(data))
+             a
+        0  4.0
+        1  8.0
+        2  3.0
+        >>> func(pl.DataFrame(data))
+        shape: (3, 1)
+        ┌─────┐
+        │ a   │
+        │ --- │
+        │ i64 │
+        ╞═════╡
+        │ 4   │
+        │ 8   │
+        │ 3   │
+        └─────┘
+        >>> func(pa.table(data))
+        pyarrow.Table
+        a: int64
+        ----
+        a: [[4,8,3]]
+    """
+    if not exprs:
+        msg = "At least one expression must be passed to `max_horizontal`"
+        raise ValueError(msg)
+    return Expr(
+        lambda plx: plx.max_horizontal(
+            *[extract_compliant(plx, v) for v in flatten(exprs)]
+        )
+    )
+
+
 class When:
     def __init__(self, *predicates: IntoExpr | Iterable[IntoExpr]) -> None:
         self._predicates = flatten([predicates])
diff --git a/narwhals/stable/v1/__init__.py b/narwhals/stable/v1/__init__.py
index 1c3a3bd1..e9aac4cf 100644
--- a/narwhals/stable/v1/__init__.py
+++ b/narwhals/stable/v1/__init__.py
@@ -1527,6 +1527,118 @@ def mean_horizontal(*exprs: IntoExpr | Iterable[IntoExpr]) -> Expr:
     return _stableify(nw.mean_horizontal(*exprs))
 
 
+def min_horizontal(*exprs: IntoExpr | Iterable[IntoExpr]) -> Expr:
+    """
+    Get the minimum value horizontally across columns.
+
+    Notes:
+        We support `min_horizontal` over numeric columns only.
+
+    Arguments:
+        exprs: Name(s) of the columns to use in the aggregation function. Accepts
+            expression input.
+
+    Examples:
+        >>> import narwhals.stable.v1 as nw
+        >>> import pandas as pd
+        >>> import polars as pl
+        >>> import pyarrow as pa
+        >>> data = {
+        ...     "a": [1, 8, 3],
+        ...     "b": [4, 5, None],
+        ...     "c": ["x", "y", "z"],
+        ... }
+
+        We define a dataframe-agnostic function that computes the horizontal min of "a"
+        and "b" columns:
+
+        >>> @nw.narwhalify
+        ... def func(df):
+        ...     return df.select(nw.min_horizontal("a", "b"))
+
+        We can then pass either pandas, polars or pyarrow to `func`:
+
+        >>> func(pd.DataFrame(data))
+             a
+        0  1.0
+        1  5.0
+        2  3.0
+        >>> func(pl.DataFrame(data))
+        shape: (3, 1)
+        ┌─────┐
+        │ a   │
+        │ --- │
+        │ i64 │
+        ╞═════╡
+        │ 1   │
+        │ 5   │
+        │ 3   │
+        └─────┘
+        >>> func(pa.table(data))
+        pyarrow.Table
+        a: int64
+        ----
+        a: [[1,5,3]]
+    """
+    return _stableify(nw.min_horizontal(*exprs))
+
+
+def max_horizontal(*exprs: IntoExpr | Iterable[IntoExpr]) -> Expr:
+    """
+    Get the maximum value horizontally across columns.
+
+    Notes:
+        We support `max_horizontal` over numeric columns only.
+
+    Arguments:
+        exprs: Name(s) of the columns to use in the aggregation function. Accepts
+            expression input.
+
+    Examples:
+        >>> import narwhals.stable.v1 as nw
+        >>> import pandas as pd
+        >>> import polars as pl
+        >>> import pyarrow as pa
+        >>> data = {
+        ...     "a": [1, 8, 3],
+        ...     "b": [4, 5, None],
+        ...     "c": ["x", "y", "z"],
+        ... }
+
+        We define a dataframe-agnostic function that computes the horizontal max of "a"
+        and "b" columns:
+
+        >>> @nw.narwhalify
+        ... def func(df):
+        ...     return df.select(nw.max_horizontal("a", "b"))
+
+        We can then pass either pandas, polars or pyarrow to `func`:
+
+        >>> func(pd.DataFrame(data))
+             a
+        0  4.0
+        1  8.0
+        2  3.0
+        >>> func(pl.DataFrame(data))
+        shape: (3, 1)
+        ┌─────┐
+        │ a   │
+        │ --- │
+        │ i64 │
+        ╞═════╡
+        │ 4   │
+        │ 8   │
+        │ 3   │
+        └─────┘
+        >>> func(pa.table(data))
+        pyarrow.Table
+        a: int64
+        ----
+        a: [[4,8,3]]
+    """
+    return _stableify(nw.max_horizontal(*exprs))
+
+
 @overload
 def concat(
     items: Iterable[DataFrame[Any]],
@@ -2153,10 +2265,12 @@ __all__ = [
     "nth",
     "len",
     "lit",
-    "min",
     "max",
+    "max_horizontal",
     "mean",
     "mean_horizontal",
+    "min",
+    "min_horizontal",
     "sum",
     "sum_horizontal",
     "when",
diff --git a/narwhals/this.py b/narwhals/this.py
new file mode 100644
index 00000000..541ee770
--- /dev/null
+++ b/narwhals/this.py
@@ -0,0 +1,17 @@
+# ruff: noqa
+ZEN = """\
+⣿⣿⣿⣿⣿⠘⣿⣿⣿⣿⣿⣿⣿⣿⣿⣿⣿⣿⣿⣿⣿⣿⣿⣿⣿⣿⣿⣿⣿⣿ THE ZEN OF NARWHALS
+⣿⣿⣿⣿⣿⠠⢹⣿⣿⣿⣿⣿⣿⣿⣿⣿⣿⣿⣿⣿⣿⣿⣿⣿⣿⣿⣿⣿⣿⣿ Keep it simple
+⣿⣿⣿⣿⣿⡀⡄⢿⣿⣿⣿⣿⣿⣿⣿⣿⣿⣿⣿⣿⣿⣿⣿⣿⣿⣿⣿⣿⣿⣿ Move slowly and keep things working
+⣿⣿⣿⣿⣿⡇⡼⡘⠛⠿⠿⠿⢿⣿⣿⣿⣿⣿⣿⣿⣿⣿⣿⣿⣿⣿⣿⣿⣿⣿ A good API is an honest one
+⣿⣿⣿⡿⣫⡄⠾⣣⠹⣿⣿⣿⣶⣮⣙⠻⢿⣿⣿⣿⣿⣿⣿⣿⣿⣿⣿⣿⣿⣿ Yes, that needs documenting
+⣿⣿⢋⣴⣿⣷⣬⣭⣾⣿⣿⣿⣿⣿⣿⣿⣦⡙⣿⣿⣿⣿⣿⣿⣿⣿⣿⣿⣿⣿ People learn better from examples
+⣿⢃⣾⣿⣿⣿⣿⣿⣿⣿⣿⣿⣿⣿⣿⣿⣿⣷⡌⢿⣿⣿⣿⣿⣿⣿⣿⣿⣿⣿ than from explanations⠀
+⡏⠀⢰⠄⢻⣿⣿⣿⣿⡿⠋⢉⠻⣿⣿⣿⣿⣿⣿⡜⣿⣿⡿⢁⢻⣿⣿⣿⣿⣿ If in doubt, say 'no'⠀
+⡇⣌⣀⣠⣾⣿⣿⣿⣿⣇⠶⠉⢁⣿⣿⣿⣿⣿⣿⣧⡹⣿⡇⣿⣧⠻⠿⠿⠿⠿ you can always reconsider⠀
+⡧⢹⣿⣿⣿⣜⣟⣸⣿⣿⣷⣶⣿⡿⣿⣿⣝⢿⣿⣿⣷⣬⣥⣿⣿⣿⣿⣿⡟⣰ Yes, we need a test for that
+⢡⣆⢻⣿⣿⣿⣿⣿⣿⣿⣿⣿⣿⣧⡙⣿⣿⡇⣿⣿⣿⣿⠟⣋⣭⣛⠻⣋⣴⣿ If you want users  
+⣶⣤⣤⣙⠻⣿⣿⣿⣿⣿⣿⣿⣿⣿⣷⣦⣍⣡⣿⡿⢋⣴⣿⣿⣿⣿⣿⣿⣿⣿ you need good docs⠀
+⣿⣿⣿⣿⣿⣶⣬⣙⣛⠻⠿⠿⠿⠿⠿⠟⣛⣩⣥⣶⣿⣿⣿⣿⣿⣿⣿⣿⣿⣿ Our code is not irreplaceable"""
+
+print(ZEN)
diff --git a/pyproject.toml b/pyproject.toml
index 5c1e6f96..c4974d8c 100644
--- a/pyproject.toml
+++ b/pyproject.toml
@@ -130,6 +130,7 @@ plugins = ["covdefaults"]
 omit = [
   'narwhals/typing.py',
   'narwhals/stable/v1/typing.py',
+  'narwhals/this.py',
   # we can run this in every environment that we measure coverage on due to upper-bound constraits
   'narwhals/_ibis/*',
 ]


Test patch:
diff --git a/tests/expr_and_series/all_horizontal_test.py b/tests/expr_and_series/all_horizontal_test.py
index 98dc9f9f..01d53fe6 100644
--- a/tests/expr_and_series/all_horizontal_test.py
+++ b/tests/expr_and_series/all_horizontal_test.py
@@ -87,3 +87,13 @@ def test_horizontal_expressions_emtpy(constructor: Constructor) -> None:
         ValueError, match=r"At least one expression must be passed.*sum_horizontal"
     ):
         df.select(nw.sum_horizontal())
+
+    with pytest.raises(
+        ValueError, match=r"At least one expression must be passed.*max_horizontal"
+    ):
+        df.select(nw.max_horizontal())
+
+    with pytest.raises(
+        ValueError, match=r"At least one expression must be passed.*min_horizontal"
+    ):
+        df.select(nw.min_horizontal())
diff --git a/tests/expr_and_series/max_horizontal_test.py b/tests/expr_and_series/max_horizontal_test.py
new file mode 100644
index 00000000..711ce4e0
--- /dev/null
+++ b/tests/expr_and_series/max_horizontal_test.py
@@ -0,0 +1,25 @@
+from typing import Any
+
+import pytest
+
+import narwhals.stable.v1 as nw
+from tests.utils import Constructor
+from tests.utils import compare_dicts
+
+data = {"a": [1, 3, None, None], "b": [4, None, 6, None], "z": [3, 1, None, None]}
+expected_values = [4, 3, 6, float("nan")]
+
+
+@pytest.mark.parametrize("col_expr", [nw.col("a"), "a"])
+def test_maxh(constructor: Constructor, col_expr: Any) -> None:
+    df = nw.from_native(constructor(data))
+    result = df.select(horizontal_max=nw.max_horizontal(col_expr, nw.col("b"), "z"))
+    expected = {"horizontal_max": expected_values}
+    compare_dicts(result, expected)
+
+
+def test_maxh_all(constructor: Constructor) -> None:
+    df = nw.from_native(constructor(data))
+    result = df.select(nw.max_horizontal(nw.all()), c=nw.max_horizontal(nw.all()))
+    expected = {"a": expected_values, "c": expected_values}
+    compare_dicts(result, expected)
diff --git a/tests/expr_and_series/min_horizontal_test.py b/tests/expr_and_series/min_horizontal_test.py
new file mode 100644
index 00000000..ca34d440
--- /dev/null
+++ b/tests/expr_and_series/min_horizontal_test.py
@@ -0,0 +1,25 @@
+from typing import Any
+
+import pytest
+
+import narwhals.stable.v1 as nw
+from tests.utils import Constructor
+from tests.utils import compare_dicts
+
+data = {"a": [1, 3, None, None], "b": [4, None, 6, None], "z": [3, 1, None, None]}
+expcted_values = [1, 1, 6, float("nan")]
+
+
+@pytest.mark.parametrize("col_expr", [nw.col("a"), "a"])
+def test_minh(constructor: Constructor, col_expr: Any) -> None:
+    df = nw.from_native(constructor(data))
+    result = df.select(horizontal_min=nw.min_horizontal(col_expr, nw.col("b"), "z"))
+    expected = {"horizontal_min": expcted_values}
+    compare_dicts(result, expected)
+
+
+def test_minh_all(constructor: Constructor) -> None:
+    df = nw.from_native(constructor(data))
+    result = df.select(nw.min_horizontal(nw.all()), c=nw.min_horizontal(nw.all()))
+    expected = {"a": expcted_values, "c": expcted_values}
+    compare_dicts(result, expected)