Repository: narwhals-dev/narwhals

Issue:
feat: add `schema` argument to `from_dict`
See https://github.com/narwhals-dev/narwhals/pull/602#discussion_r1688654218 for context



Discussion:

Patch:
diff --git a/narwhals/_polars/namespace.py b/narwhals/_polars/namespace.py
index 96a97132..d5c8307f 100644
--- a/narwhals/_polars/namespace.py
+++ b/narwhals/_polars/namespace.py
@@ -55,7 +55,7 @@ class PolarsNamespace:
         from narwhals._polars.expr import PolarsExpr
 
         pl = get_polars()
-        if self._backend_version < (0, 20, 4):  # pragma: no cover
+        if self._backend_version < (0, 20, 5):  # pragma: no cover
             return PolarsExpr(pl.count().alias("len"))
         return PolarsExpr(pl.len())
 
diff --git a/narwhals/functions.py b/narwhals/functions.py
index 9db6fdba..40316f67 100644
--- a/narwhals/functions.py
+++ b/narwhals/functions.py
@@ -25,6 +25,8 @@ from narwhals.utils import validate_laziness
 FrameT = TypeVar("FrameT", bound=Union[DataFrame, LazyFrame])  # type: ignore[type-arg]
 
 if TYPE_CHECKING:
+    from narwhals.dtypes import DType
+    from narwhals.schema import Schema
     from narwhals.series import Series
 
 
@@ -48,12 +50,22 @@ def concat(
     )
 
 
-def from_dict(data: dict[str, Any], *, native_namespace: Any) -> DataFrame[Any]:
+def from_dict(
+    data: dict[str, Any],
+    schema: dict[str, DType] | Schema | None = None,
+    *,
+    native_namespace: Any,
+) -> DataFrame[Any]:
     """
     Instantiate DataFrame from dictionary.
 
+    Notes:
+        For pandas-like dataframes, conversion to schema is applied after dataframe
+        creation.
+
     Arguments:
         data: Dictionary to create DataFrame from.
+        schema: The DataFrame schema as Schema or dict of {name: type}.
         native_namespace: The native library to use for DataFrame creation.
 
     Examples:
@@ -88,15 +100,58 @@ def from_dict(data: dict[str, Any], *, native_namespace: Any) -> DataFrame[Any]:
         └─────┴─────┘
     """
     if native_namespace is get_polars():
-        native_frame = native_namespace.from_dict(data)
+        if schema:
+            from narwhals._polars.utils import (
+                reverse_translate_dtype as polars_reverse_translate_dtype,
+            )
+
+            schema = {
+                name: polars_reverse_translate_dtype(dtype)
+                for name, dtype in schema.items()
+            }
+
+        native_frame = native_namespace.from_dict(data, schema=schema)
     elif (
         native_namespace is get_cudf()
         or native_namespace is get_modin()
         or native_namespace is get_pandas()
     ):
         native_frame = native_namespace.DataFrame.from_dict(data)
+
+        if schema:
+            from narwhals._pandas_like.utils import Implementation
+            from narwhals._pandas_like.utils import (
+                reverse_translate_dtype as pandas_like_reverse_translate_dtype,
+            )
+
+            implementation = (
+                Implementation.PANDAS
+                if native_namespace is get_pandas()
+                else Implementation.MODIN
+                if native_namespace is get_modin()
+                else Implementation.CUDF
+            )
+            schema = {
+                name: pandas_like_reverse_translate_dtype(
+                    schema[name], native_type, implementation
+                )
+                for name, native_type in native_frame.dtypes.items()
+            }
+            native_frame = native_frame.astype(schema)
+
     elif native_namespace is get_pyarrow():
-        native_frame = native_namespace.table(data)
+        if schema:
+            from narwhals._arrow.utils import (
+                reverse_translate_dtype as arrow_reverse_translate_dtype,
+            )
+
+            schema = native_namespace.schema(
+                [
+                    (name, arrow_reverse_translate_dtype(dtype))
+                    for name, dtype in schema.items()
+                ]
+            )
+        native_frame = native_namespace.table(data, schema=schema)
     else:  # pragma: no cover
         msg = f"Expected library supported by Narwhals, got: {native_namespace}"
         raise ValueError(msg)
diff --git a/narwhals/stable/v1.py b/narwhals/stable/v1.py
index b53596e3..8d72df2f 100644
--- a/narwhals/stable/v1.py
+++ b/narwhals/stable/v1.py
@@ -1459,12 +1459,22 @@ def get_level(
     return nw.get_level(obj)
 
 
-def from_dict(data: dict[str, Any], *, native_namespace: Any) -> DataFrame[Any]:
+def from_dict(
+    data: dict[str, Any],
+    schema: dict[str, DType] | Schema | None = None,
+    *,
+    native_namespace: Any,
+) -> DataFrame[Any]:
     """
     Instantiate DataFrame from dictionary.
 
+    Notes:
+        For pandas-like dataframes, conversion to schema is applied after dataframe
+        creation.
+
     Arguments:
         data: Dictionary to create DataFrame from.
+        schema: The DataFrame schema as Schema or dict of {name: type}.
         native_namespace: The native library to use for DataFrame creation.
 
     Examples:
@@ -1498,7 +1508,9 @@ def from_dict(data: dict[str, Any], *, native_namespace: Any) -> DataFrame[Any]:
         │ 2   ┆ 4   │
         └─────┴─────┘
     """
-    return _stableify(nw.from_dict(data, native_namespace=native_namespace))  # type: ignore[no-any-return]
+    return _stableify(  # type: ignore[no-any-return]
+        nw.from_dict(data, schema=schema, native_namespace=native_namespace)
+    )
 
 
 __all__ = [


Test patch:
diff --git a/tests/from_dict_test.py b/tests/from_dict_test.py
index faed3c13..5954b90a 100644
--- a/tests/from_dict_test.py
+++ b/tests/from_dict_test.py
@@ -11,3 +11,15 @@ def test_from_dict(constructor: Any) -> None:
     expected = {"c": [1, 2], "d": [5, 6]}
     compare_dicts(result, expected)
     assert isinstance(result, nw.DataFrame)
+
+
+def test_from_dict_schema(constructor: Any) -> None:
+    schema = {"c": nw.Int16(), "d": nw.Float32()}
+    df = nw.from_native(constructor({"a": [1, 2, 3], "b": [4, 5, 6]}), eager_only=True)
+    native_namespace = nw.get_native_namespace(df)
+    result = nw.from_dict(
+        {"c": [1, 2], "d": [5, 6]},
+        native_namespace=native_namespace,
+        schema=schema,  # type: ignore[arg-type]
+    )
+    assert result.collect_schema() == schema