Repository: dask/dask

Issue:
Make it easier to manually create DataFrames
We often need to create [Minimal Complete Verifiable Examples](https://matthewrocklin.com/blog/work/2018/02/28/minimal-bug-reports) with Dask DataFrames for blog posts, questions, etc.

Here's how Dask allows you to make these examples currently:

```python
import pandas as pd

pandas_df = pd.DataFrame(
    {"num1": [1, 2, 3, 4], "num2": [7, 8, 9, 10]},
)
df = dd.from_pandas(pandas_df, npartitions=2)
```

This is confusing for some users.  They are not sure why pandas needs to be imported and can struggle making the pandas => Dask connection.

The following syntax would make the MCVEs shorter and easier to understand:

```python
import dask.dataframe as dd

ddf = dd.DataFrame(
    {"num1": [1, 2, 3, 4], "num2": [7, 8, 9, 10]},
    npartitions=2,
)
```

Let me know if this suggestion sounds good and I can submit a PR.  Or perhaps there is already a way to directly create a Dask DataFrame in a similar manner?


Discussion:
@jrbourbeau What do you think about this feature?
Noting as related: https://github.com/dask/dask/issues/6577#issuecomment-1113963312 :)
Thanks @MrPowers, I think this is a reasonable request but wouldn't want to see us point users directly to the `dd.DataFrame` constructor as it's a fairly low-level class constructor and we prefer to route users towards more friendly methods like `dd.read_parquet`, `dd.from_delayed`, etc.

That said, adding a new `dd.from_dict` method similar to pandas' [`pd.from_dict`](https://pandas.pydata.org/docs/reference/api/pandas.DataFrame.from_dict.html) seems like a nice way to accomplish what you're after while also sticking with an existing DataFrame API. Adding this new method would be my personal preference 
Also, there is a [`dask.datasets` module](https://github.com/dask/dask/blob/298112115f63bd8846527bfafae2886999f0f4ec/dask/datasets.py) where we maintain a couple of utility functions for generating demo DataFrames. These might help in some situations, though not all, I'm mostly just mentioning as an FYI. 
@jrbourbeau - `dd.from_dict` sounds like a great approach to me.  I'll prep a PR.  Thanks!

Resolving patch:
diff --git a/dask/dataframe/core.py b/dask/dataframe/core.py
--- a/dask/dataframe/core.py
+++ b/dask/dataframe/core.py
@@ -5750,6 +5750,50 @@ def _is_column_label_reference(self, key):
             and key in self.columns
         )
 
+    @classmethod
+    def from_dict(
+        cls, data, *, npartitions, orient="columns", dtype=None, columns=None
+    ):
+        """
+        Construct a Dask DataFrame from a Python Dictionary
+
+        Parameters
+        ----------
+        data : dict
+            Of the form {field : array-like} or {field : dict}.
+        npartitions : int
+            The number of partitions of the index to create. Note that depending on
+            the size and index of the dataframe, the output may have fewer
+            partitions than requested.
+        orient : {'columns', 'index', 'tight'}, default 'columns'
+            The "orientation" of the data. If the keys of the passed dict
+            should be the columns of the resulting DataFrame, pass 'columns'
+            (default). Otherwise if the keys should be rows, pass 'index'.
+            If 'tight', assume a dict with keys
+            ['index', 'columns', 'data', 'index_names', 'column_names'].
+        dtype: bool
+            Data type to force, otherwise infer.
+        columns: string, optional
+            Column labels to use when ``orient='index'``. Raises a ValueError
+            if used with ``orient='columns'`` or ``orient='tight'``.
+
+        Examples
+        --------
+        >>> import dask.dataframe as dd
+        >>> ddf = dd.DataFrame.from_dict({"num1": [1, 2, 3, 4], "num2": [7, 8, 9, 10]}, npartitions=2)
+        """
+        from dask.dataframe.io import from_pandas
+
+        collection_types = {type(v) for v in data.values() if is_dask_collection(v)}
+        if collection_types:
+            raise NotImplementedError(
+                "from_dict doesn't currently support Dask collections as inputs. "
+                f"Objects of type {collection_types} were given in the input dict."
+            )
+        pdf = pd.DataFrame.from_dict(data, orient, dtype, columns)
+        ddf = from_pandas(pdf, npartitions)
+        return ddf
+
 
 # bind operators
 # TODO: dynamically bound operators are defeating type annotations


Test patch:
diff --git a/dask/dataframe/tests/test_dataframe.py b/dask/dataframe/tests/test_dataframe.py
--- a/dask/dataframe/tests/test_dataframe.py
+++ b/dask/dataframe/tests/test_dataframe.py
@@ -5164,3 +5164,27 @@ def reduce_fn(series):
         .compute()[0]
     )
     assert result == {"x": 14, "y": 64}
+
+
+@pytest.mark.parametrize("dtype", [int, float])
+@pytest.mark.parametrize("orient", ["columns", "index"])
+@pytest.mark.parametrize("npartitions", [2, 5])
+def test_from_dict(dtype, orient, npartitions):
+    data = {"a": range(10), "b": range(10)}
+    expected = pd.DataFrame.from_dict(data, dtype=dtype, orient=orient)
+    result = dd.DataFrame.from_dict(
+        data, npartitions=npartitions, dtype=dtype, orient=orient
+    )
+    if orient == "index":
+        # DataFrame only has two rows with this orientation
+        assert result.npartitions == 1
+    else:
+        assert result.npartitions == npartitions
+    assert_eq(result, expected)
+
+
+def test_from_dict_raises():
+    s = pd.Series(range(10))
+    ds = dd.from_pandas(s, npartitions=2)
+    with pytest.raises(NotImplementedError, match="Dask collections as inputs"):
+        dd.DataFrame.from_dict({"a": ds}, npartitions=2)