Repository: narwhals-dev/narwhals

Issue:
enh: create dataframe from 2D numpy array and column names
For classical numpy dtypes in pandas, this should be more efficient than forcing slices + dicts

Spotted in Prophet

Desired syntax:
```python
import narwhals as nw
import numpy as np
import pandas as pd

arr = np.zeros([[1,2,3], [4,5,6]])
df = nw.from_2d_array(arr, column_names=['a', 'b', 'c'])
```
It should also be possible to pass in `dtype` as an argument - if not passed, it'll be autoinferred
```python
import narwhals as nw
import numpy as np
import pandas as pd

arr = np.zeros([[1,2,3], [4,5,6]])
df = nw.from_2d_array(arr, column_names=['a', 'b', 'c'], dtype=nw.Float64)
```

Discussion:

Patch:
diff --git a/docs/api-reference/narwhals.md b/docs/api-reference/narwhals.md
index 165af7f3..d1423b9c 100644
--- a/docs/api-reference/narwhals.md
+++ b/docs/api-reference/narwhals.md
@@ -12,9 +12,10 @@ Here are the top-level functions available in Narwhals.
         - col
         - concat
         - concat_str
+        - from_arrow
         - from_dict
         - from_native
-        - from_arrow
+        - from_numpy
         - generate_temporary_column_name
         - get_level
         - get_native_namespace
@@ -38,8 +39,8 @@ Here are the top-level functions available in Narwhals.
         - nth
         - sum
         - sum_horizontal
-        - when
         - show_versions
         - to_native
         - to_py_scalar
+        - when
       show_source: false
diff --git a/narwhals/__init__.py b/narwhals/__init__.py
index ca299e25..449b416f 100644
--- a/narwhals/__init__.py
+++ b/narwhals/__init__.py
@@ -52,6 +52,7 @@ from narwhals.expr import when
 from narwhals.functions import concat
 from narwhals.functions import from_arrow
 from narwhals.functions import from_dict
+from narwhals.functions import from_numpy
 from narwhals.functions import get_level
 from narwhals.functions import new_series
 from narwhals.functions import show_versions
@@ -112,7 +113,7 @@ __all__ = [
     "exceptions",
     "from_arrow",
     "from_dict",
-    "from_dict",
+    "from_numpy",
     "from_native",
     "generate_temporary_column_name",
     "get_level",
diff --git a/narwhals/functions.py b/narwhals/functions.py
index 42b235b5..a7cf3f70 100644
--- a/narwhals/functions.py
+++ b/narwhals/functions.py
@@ -26,6 +26,8 @@ FrameT = TypeVar("FrameT", bound=Union[DataFrame, LazyFrame])  # type: ignore[ty
 if TYPE_CHECKING:
     from types import ModuleType
 
+    import numpy as np
+
     from narwhals.dtypes import DType
     from narwhals.schema import Schema
     from narwhals.series import Series
@@ -336,7 +338,7 @@ def from_dict(
             necessary if inputs are not Narwhals Series.
 
     Returns:
-        A new DataFrame
+        A new DataFrame.
 
     Examples:
         >>> import pandas as pd
@@ -353,7 +355,7 @@ def from_dict(
         ...     native_namespace = nw.get_native_namespace(df)
         ...     return nw.from_dict(new_data, native_namespace=native_namespace)
 
-        Let's see what happens when passing Pandas, Polars or PyArrow input:
+        Let's see what happens when passing pandas, Polars or PyArrow input:
 
         >>> func(pd.DataFrame(data))
            c  d
@@ -463,13 +465,269 @@ def _from_dict_impl(
         try:
             # implementation is UNKNOWN, Narwhals extension using this feature should
             # implement `from_dict` function in the top-level namespace.
-            native_frame = native_namespace.from_dict(data)
+            native_frame = native_namespace.from_dict(data, schema=schema)
         except AttributeError as e:
             msg = "Unknown namespace is expected to implement `from_dict` function."
             raise AttributeError(msg) from e
     return from_native(native_frame, eager_only=True)
 
 
+def from_numpy(
+    data: np.ndarray,
+    schema: dict[str, DType] | Schema | list[str] | None = None,
+    *,
+    native_namespace: ModuleType,
+) -> DataFrame[Any]:
+    """Construct a DataFrame from a NumPy ndarray.
+
+    Notes:
+        Only row orientation is currently supported.
+
+        For pandas-like dataframes, conversion to schema is applied after dataframe
+        creation.
+
+    Arguments:
+        data: Two-dimensional data represented as a NumPy ndarray.
+        schema: The DataFrame schema as Schema, dict of {name: type}, or a list of str.
+        native_namespace: The native library to use for DataFrame creation.
+
+    Returns:
+        A new DataFrame.
+
+    Examples:
+        >>> import pandas as pd
+        >>> import polars as pl
+        >>> import pyarrow as pa
+        >>> import narwhals as nw
+        >>> import numpy as np
+        >>> from narwhals.typing import IntoFrameT
+        >>> data = {"a": [1, 2], "b": [3, 4]}
+
+        Let's create a new dataframe of the same class as the dataframe we started with, from a NumPy ndarray of new data:
+
+        >>> def agnostic_from_numpy(df_native: IntoFrameT) -> IntoFrameT:
+        ...     new_data = np.array([[5, 2, 1], [1, 4, 3]])
+        ...     df = nw.from_native(df_native)
+        ...     native_namespace = nw.get_native_namespace(df)
+        ...     return nw.from_numpy(new_data, native_namespace=native_namespace).to_native()
+
+        Let's see what happens when passing pandas, Polars or PyArrow input:
+
+        >>> agnostic_from_numpy(pd.DataFrame(data))
+           column_0  column_1  column_2
+        0         5         2         1
+        1         1         4         3
+        >>> agnostic_from_numpy(pl.DataFrame(data))
+        shape: (2, 3)
+        ┌──────────┬──────────┬──────────┐
+        │ column_0 ┆ column_1 ┆ column_2 │
+        │ ---      ┆ ---      ┆ ---      │
+        │ i64      ┆ i64      ┆ i64      │
+        ╞══════════╪══════════╪══════════╡
+        │ 5        ┆ 2        ┆ 1        │
+        │ 1        ┆ 4        ┆ 3        │
+        └──────────┴──────────┴──────────┘
+        >>> agnostic_from_numpy(pa.table(data))
+        pyarrow.Table
+        column_0: int64
+        column_1: int64
+        column_2: int64
+        ----
+        column_0: [[5,1]]
+        column_1: [[2,4]]
+        column_2: [[1,3]]
+
+        Let's specify the column names:
+
+        >>> def agnostic_from_numpy(df_native: IntoFrameT) -> IntoFrameT:
+        ...     new_data = np.array([[5, 2, 1], [1, 4, 3]])
+        ...     schema = ["c", "d", "e"]
+        ...     df = nw.from_native(df_native)
+        ...     native_namespace = nw.get_native_namespace(df)
+        ...     return nw.from_numpy(
+        ...         new_data, native_namespace=native_namespace, schema=schema
+        ...     ).to_native()
+
+        Let's see the modified outputs:
+
+        >>> agnostic_from_numpy(pd.DataFrame(data))
+           c  d  e
+        0  5  2  1
+        1  1  4  3
+        >>> agnostic_from_numpy(pl.DataFrame(data))
+        shape: (2, 3)
+        ┌─────┬─────┬─────┐
+        │ c   ┆ d   ┆ e   │
+        │ --- ┆ --- ┆ --- │
+        │ i64 ┆ i64 ┆ i64 │
+        ╞═════╪═════╪═════╡
+        │ 5   ┆ 2   ┆ 1   │
+        │ 1   ┆ 4   ┆ 3   │
+        └─────┴─────┴─────┘
+        >>> agnostic_from_numpy(pa.table(data))
+        pyarrow.Table
+        c: int64
+        d: int64
+        e: int64
+        ----
+        c: [[5,1]]
+        d: [[2,4]]
+        e: [[1,3]]
+
+        Let's modify the function so that it specifies the schema:
+
+        >>> def agnostic_from_numpy(df_native: IntoFrameT) -> IntoFrameT:
+        ...     new_data = np.array([[5, 2, 1], [1, 4, 3]])
+        ...     schema = {"c": nw.Int16(), "d": nw.Float32(), "e": nw.Int8()}
+        ...     df = nw.from_native(df_native)
+        ...     native_namespace = nw.get_native_namespace(df)
+        ...     return nw.from_numpy(
+        ...         new_data, native_namespace=native_namespace, schema=schema
+        ...     ).to_native()
+
+        Let's see the outputs:
+
+        >>> agnostic_from_numpy(pd.DataFrame(data))
+           c    d  e
+        0  5  2.0  1
+        1  1  4.0  3
+        >>> agnostic_from_numpy(pl.DataFrame(data))
+        shape: (2, 3)
+        ┌─────┬─────┬─────┐
+        │ c   ┆ d   ┆ e   │
+        │ --- ┆ --- ┆ --- │
+        │ i16 ┆ f32 ┆ i8  │
+        ╞═════╪═════╪═════╡
+        │ 5   ┆ 2.0 ┆ 1   │
+        │ 1   ┆ 4.0 ┆ 3   │
+        └─────┴─────┴─────┘
+        >>> agnostic_from_numpy(pa.table(data))
+        pyarrow.Table
+        c: int16
+        d: float
+        e: int8
+        ----
+        c: [[5,1]]
+        d: [[2,4]]
+        e: [[1,3]]
+    """
+    from narwhals import dtypes
+
+    return _from_numpy_impl(
+        data,
+        schema,
+        native_namespace=native_namespace,
+        dtypes=dtypes,  # type: ignore[arg-type]
+    )
+
+
+def _from_numpy_impl(
+    data: np.ndarray,
+    schema: dict[str, DType] | Schema | list[str] | None = None,
+    *,
+    native_namespace: ModuleType,
+    dtypes: DTypes,
+) -> DataFrame[Any]:
+    from narwhals.schema import Schema
+
+    if data.ndim != 2:
+        msg = "`from_numpy` only accepts 2D numpy arrays"
+        raise ValueError(msg)
+    implementation = Implementation.from_native_namespace(native_namespace)
+
+    if implementation is Implementation.POLARS:
+        if isinstance(schema, (dict, Schema)):
+            from narwhals._polars.utils import (
+                narwhals_to_native_dtype as polars_narwhals_to_native_dtype,
+            )
+
+            schema = {
+                name: polars_narwhals_to_native_dtype(dtype, dtypes=dtypes)  # type: ignore[misc]
+                for name, dtype in schema.items()
+            }
+        elif schema is None:
+            native_frame = native_namespace.from_numpy(data)
+        elif not isinstance(schema, list):
+            msg = (
+                "`schema` is expected to be one of the following types: "
+                "dict[str, DType] | Schema | list[str]. "
+                f"Got {type(schema)}."
+            )
+            raise TypeError(msg)
+        native_frame = native_namespace.from_numpy(data, schema=schema)
+
+    elif implementation in {
+        Implementation.PANDAS,
+        Implementation.MODIN,
+        Implementation.CUDF,
+    }:
+        if isinstance(schema, (dict, Schema)):
+            from narwhals._pandas_like.utils import (
+                narwhals_to_native_dtype as pandas_like_narwhals_to_native_dtype,
+            )
+
+            backend_version = parse_version(native_namespace.__version__)
+            schema = {
+                name: pandas_like_narwhals_to_native_dtype(
+                    schema[name], native_type, implementation, backend_version, dtypes
+                )
+                for name, native_type in schema.items()
+            }
+            native_frame = native_namespace.DataFrame(data, columns=schema.keys()).astype(
+                schema
+            )
+        elif isinstance(schema, list):
+            native_frame = native_namespace.DataFrame(data, columns=schema)
+        elif schema is None:
+            native_frame = native_namespace.DataFrame(
+                data, columns=["column_" + str(x) for x in range(data.shape[1])]
+            )
+        else:
+            msg = (
+                "`schema` is expected to be one of the following types: "
+                "dict[str, DType] | Schema | list[str]. "
+                f"Got {type(schema)}."
+            )
+            raise TypeError(msg)
+
+    elif implementation is Implementation.PYARROW:
+        pa_arrays = [native_namespace.array(val) for val in data.T]
+        if isinstance(schema, (dict, Schema)):
+            from narwhals._arrow.utils import (
+                narwhals_to_native_dtype as arrow_narwhals_to_native_dtype,
+            )
+
+            schema = native_namespace.schema(
+                [
+                    (name, arrow_narwhals_to_native_dtype(dtype, dtypes))
+                    for name, dtype in schema.items()
+                ]
+            )
+            native_frame = native_namespace.Table.from_arrays(pa_arrays, schema=schema)
+        elif isinstance(schema, list):
+            native_frame = native_namespace.Table.from_arrays(pa_arrays, names=schema)
+        elif schema is None:
+            native_frame = native_namespace.Table.from_arrays(
+                pa_arrays, names=["column_" + str(x) for x in range(data.shape[1])]
+            )
+        else:
+            msg = (
+                "`schema` is expected to be one of the following types: "
+                "dict[str, DType] | Schema | list[str]. "
+                f"Got {type(schema)}."
+            )
+            raise TypeError(msg)
+    else:  # pragma: no cover
+        try:
+            # implementation is UNKNOWN, Narwhals extension using this feature should
+            # implement `from_numpy` function in the top-level namespace.
+            native_frame = native_namespace.from_numpy(data, schema=schema)
+        except AttributeError as e:
+            msg = "Unknown namespace is expected to implement `from_numpy` function."
+            raise AttributeError(msg) from e
+    return from_native(native_frame, eager_only=True)
+
+
 def from_arrow(
     native_frame: ArrowStreamExportable, *, native_namespace: ModuleType
 ) -> DataFrame[Any]:
@@ -480,7 +738,7 @@ def from_arrow(
         native_namespace: The native library to use for DataFrame creation.
 
     Returns:
-        A new DataFrame
+        A new DataFrame.
 
     Examples:
         >>> import pandas as pd
diff --git a/narwhals/stable/v1/__init__.py b/narwhals/stable/v1/__init__.py
index 53f4cd0c..bef5d1ec 100644
--- a/narwhals/stable/v1/__init__.py
+++ b/narwhals/stable/v1/__init__.py
@@ -22,6 +22,7 @@ from narwhals.expr import Then as NwThen
 from narwhals.expr import When as NwWhen
 from narwhals.expr import when as nw_when
 from narwhals.functions import _from_dict_impl
+from narwhals.functions import _from_numpy_impl
 from narwhals.functions import _new_series_impl
 from narwhals.functions import from_arrow as nw_from_arrow
 from narwhals.functions import get_level
@@ -70,6 +71,7 @@ from narwhals.utils import validate_strict_and_pass_though
 if TYPE_CHECKING:
     from types import ModuleType
 
+    import numpy as np
     from typing_extensions import Self
 
     from narwhals.dtypes import DType
@@ -3127,7 +3129,7 @@ def from_arrow(
         native_namespace: The native library to use for DataFrame creation.
 
     Returns:
-        A new DataFrame
+        A new DataFrame.
 
     Examples:
         >>> import pandas as pd
@@ -3184,7 +3186,7 @@ def from_dict(
             necessary if inputs are not Narwhals Series.
 
     Returns:
-        A new DataFrame
+        A new DataFrame.
 
     Examples:
         >>> import pandas as pd
@@ -3201,7 +3203,7 @@ def from_dict(
         ...     native_namespace = nw.get_native_namespace(df)
         ...     return nw.from_dict(new_data, native_namespace=native_namespace)
 
-        Let's see what happens when passing Pandas, Polars or PyArrow input:
+        Let's see what happens when passing pandas, Polars or PyArrow input:
 
         >>> func(pd.DataFrame(data))
            c  d
@@ -3237,6 +3239,157 @@ def from_dict(
     )
 
 
+def from_numpy(
+    data: np.ndarray,
+    schema: dict[str, DType] | Schema | list[str] | None = None,
+    *,
+    native_namespace: ModuleType,
+) -> DataFrame[Any]:
+    """Construct a DataFrame from a NumPy ndarray.
+
+    Notes:
+        Only row orientation is currently supported.
+
+        For pandas-like dataframes, conversion to schema is applied after dataframe
+        creation.
+
+    Arguments:
+        data: Two-dimensional data represented as a NumPy ndarray.
+        schema: The DataFrame schema as Schema, dict of {name: type}, or a list of str.
+        native_namespace: The native library to use for DataFrame creation.
+
+    Returns:
+        A new DataFrame.
+
+    Examples:
+        >>> import pandas as pd
+        >>> import polars as pl
+        >>> import pyarrow as pa
+        >>> import narwhals as nw
+        >>> import numpy as np
+        >>> from narwhals.typing import IntoFrameT
+        >>> data = {"a": [1, 2], "b": [3, 4]}
+
+        Let's create a new dataframe of the same class as the dataframe we started with, from a NumPy ndarray of new data:
+
+        >>> def agnostic_from_numpy(df_native: IntoFrameT) -> IntoFrameT:
+        ...     new_data = np.array([[5, 2, 1], [1, 4, 3]])
+        ...     df = nw.from_native(df_native)
+        ...     native_namespace = nw.get_native_namespace(df)
+        ...     return nw.from_numpy(new_data, native_namespace=native_namespace).to_native()
+
+        Let's see what happens when passing pandas, Polars or PyArrow input:
+
+        >>> agnostic_from_numpy(pd.DataFrame(data))
+           column_0  column_1  column_2
+        0         5         2         1
+        1         1         4         3
+        >>> agnostic_from_numpy(pl.DataFrame(data))
+        shape: (2, 3)
+        ┌──────────┬──────────┬──────────┐
+        │ column_0 ┆ column_1 ┆ column_2 │
+        │ ---      ┆ ---      ┆ ---      │
+        │ i64      ┆ i64      ┆ i64      │
+        ╞══════════╪══════════╪══════════╡
+        │ 5        ┆ 2        ┆ 1        │
+        │ 1        ┆ 4        ┆ 3        │
+        └──────────┴──────────┴──────────┘
+        >>> agnostic_from_numpy(pa.table(data))
+        pyarrow.Table
+        column_0: int64
+        column_1: int64
+        column_2: int64
+        ----
+        column_0: [[5,1]]
+        column_1: [[2,4]]
+        column_2: [[1,3]]
+
+        Let's specify the column names:
+
+        >>> def agnostic_from_numpy(df_native: IntoFrameT) -> IntoFrameT:
+        ...     new_data = np.array([[5, 2, 1], [1, 4, 3]])
+        ...     schema = ["c", "d", "e"]
+        ...     df = nw.from_native(df_native)
+        ...     native_namespace = nw.get_native_namespace(df)
+        ...     return nw.from_numpy(
+        ...         new_data, native_namespace=native_namespace, schema=schema
+        ...     ).to_native()
+
+        Let's see the modified outputs:
+
+        >>> agnostic_from_numpy(pd.DataFrame(data))
+           c  d  e
+        0  5  2  1
+        1  1  4  3
+        >>> agnostic_from_numpy(pl.DataFrame(data))
+        shape: (2, 3)
+        ┌─────┬─────┬─────┐
+        │ c   ┆ d   ┆ e   │
+        │ --- ┆ --- ┆ --- │
+        │ i64 ┆ i64 ┆ i64 │
+        ╞═════╪═════╪═════╡
+        │ 5   ┆ 2   ┆ 1   │
+        │ 1   ┆ 4   ┆ 3   │
+        └─────┴─────┴─────┘
+        >>> agnostic_from_numpy(pa.table(data))
+        pyarrow.Table
+        c: int64
+        d: int64
+        e: int64
+        ----
+        c: [[5,1]]
+        d: [[2,4]]
+        e: [[1,3]]
+
+        Let's modify the function so that it specifies the schema:
+
+        >>> def agnostic_from_numpy(df_native: IntoFrameT) -> IntoFrameT:
+        ...     new_data = np.array([[5, 2, 1], [1, 4, 3]])
+        ...     schema = {"c": nw.Int16(), "d": nw.Float32(), "e": nw.Int8()}
+        ...     df = nw.from_native(df_native)
+        ...     native_namespace = nw.get_native_namespace(df)
+        ...     return nw.from_numpy(
+        ...         new_data, native_namespace=native_namespace, schema=schema
+        ...     ).to_native()
+
+        Let's see the outputs:
+
+        >>> agnostic_from_numpy(pd.DataFrame(data))
+           c    d  e
+        0  5  2.0  1
+        1  1  4.0  3
+        >>> agnostic_from_numpy(pl.DataFrame(data))
+        shape: (2, 3)
+        ┌─────┬─────┬─────┐
+        │ c   ┆ d   ┆ e   │
+        │ --- ┆ --- ┆ --- │
+        │ i16 ┆ f32 ┆ i8  │
+        ╞═════╪═════╪═════╡
+        │ 5   ┆ 2.0 ┆ 1   │
+        │ 1   ┆ 4.0 ┆ 3   │
+        └─────┴─────┴─────┘
+        >>> agnostic_from_numpy(pa.table(data))
+        pyarrow.Table
+        c: int16
+        d: float
+        e: int8
+        ----
+        c: [[5,1]]
+        d: [[2,4]]
+        e: [[1,3]]
+    """
+    from narwhals.stable.v1 import dtypes
+
+    return _stableify(
+        _from_numpy_impl(
+            data,
+            schema,
+            native_namespace=native_namespace,
+            dtypes=dtypes,  # type: ignore[arg-type]
+        )
+    )
+
+
 __all__ = [
     "Array",
     "Boolean",
@@ -3278,6 +3431,7 @@ __all__ = [
     "from_arrow",
     "from_dict",
     "from_native",
+    "from_numpy",
     "generate_temporary_column_name",
     "get_level",
     "get_native_namespace",


Test patch:
diff --git a/.github/workflows/downstream_tests.yml b/.github/workflows/downstream_tests.yml
index c8a1cbcc..fa3265f8 100644
--- a/.github/workflows/downstream_tests.yml
+++ b/.github/workflows/downstream_tests.yml
@@ -184,6 +184,8 @@ jobs:
             make narwhals-install-shiny
       - name: install-narwhals-dev
         run: |
+            # temporary pin to get CI green
+            uv pip install "ollama < 0.4.2" --system
             uv pip uninstall narwhals --system
             uv pip install -e . --system
       - name: show-deps
diff --git a/tests/from_numpy_test.py b/tests/from_numpy_test.py
new file mode 100644
index 00000000..b736d5cb
--- /dev/null
+++ b/tests/from_numpy_test.py
@@ -0,0 +1,95 @@
+from __future__ import annotations
+
+import numpy as np
+import pytest
+
+import narwhals as nw
+import narwhals.stable.v1 as nw_v1
+from tests.utils import Constructor
+from tests.utils import assert_equal_data
+
+data = {"a": [1, 2, 3], "b": [4, 5, 6]}
+arr = np.array([[5, 2, 0, 1], [1, 4, 7, 8], [1, 2, 3, 9]])
+expected = {
+    "column_0": [5, 1, 1],
+    "column_1": [2, 4, 2],
+    "column_2": [0, 7, 3],
+    "column_3": [1, 8, 9],
+}
+
+
+def test_from_numpy(constructor: Constructor, request: pytest.FixtureRequest) -> None:
+    if "dask" in str(constructor):
+        request.applymarker(pytest.mark.xfail)
+    df = nw.from_native(constructor(data))
+    native_namespace = nw.get_native_namespace(df)
+    result = nw.from_numpy(arr, native_namespace=native_namespace)
+    assert_equal_data(result, expected)
+    assert isinstance(result, nw.DataFrame)
+
+
+def test_from_numpy_schema_dict(
+    constructor: Constructor, request: pytest.FixtureRequest
+) -> None:
+    if "dask" in str(constructor):
+        request.applymarker(pytest.mark.xfail)
+    schema = {
+        "c": nw_v1.Int16(),
+        "d": nw_v1.Float32(),
+        "e": nw_v1.Int16(),
+        "f": nw_v1.Float64(),
+    }
+    df = nw_v1.from_native(constructor(data))
+    native_namespace = nw_v1.get_native_namespace(df)
+    result = nw_v1.from_numpy(
+        arr,
+        native_namespace=native_namespace,
+        schema=schema,  # type: ignore[arg-type]
+    )
+    assert result.collect_schema() == schema
+
+
+def test_from_numpy_schema_list(
+    constructor: Constructor, request: pytest.FixtureRequest
+) -> None:
+    if "dask" in str(constructor):
+        request.applymarker(pytest.mark.xfail)
+    schema = ["c", "d", "e", "f"]
+    df = nw_v1.from_native(constructor(data))
+    native_namespace = nw_v1.get_native_namespace(df)
+    result = nw_v1.from_numpy(
+        arr,
+        native_namespace=native_namespace,
+        schema=schema,
+    )
+    assert result.columns == schema
+
+
+def test_from_numpy_schema_notvalid(
+    constructor: Constructor, request: pytest.FixtureRequest
+) -> None:
+    if "dask" in str(constructor):
+        request.applymarker(pytest.mark.xfail)
+    df = nw.from_native(constructor(data))
+    native_namespace = nw_v1.get_native_namespace(df)
+    with pytest.raises(
+        TypeError, match="`schema` is expected to be one of the following types"
+    ):
+        nw.from_numpy(arr, schema="a", native_namespace=native_namespace)  # type: ignore[arg-type]
+
+
+def test_from_numpy_v1(constructor: Constructor, request: pytest.FixtureRequest) -> None:
+    if "dask" in str(constructor):
+        request.applymarker(pytest.mark.xfail)
+    df = nw_v1.from_native(constructor(data))
+    native_namespace = nw_v1.get_native_namespace(df)
+    result = nw_v1.from_numpy(arr, native_namespace=native_namespace)
+    assert_equal_data(result, expected)
+    assert isinstance(result, nw_v1.DataFrame)
+
+
+def test_from_numpy_not2d(constructor: Constructor) -> None:
+    df = nw.from_native(constructor(data))
+    native_namespace = nw_v1.get_native_namespace(df)
+    with pytest.raises(ValueError, match="`from_numpy` only accepts 2D numpy arrays"):
+        nw.from_numpy(np.array([0]), native_namespace=native_namespace)