Repository: pandas-dev/pandas
Pull request: ENH: Add index param to df.to_dict
- [x] closes #46398
- [x] [Tests added and passed](https://pandas.pydata.org/pandas-docs/dev/development/contributing_codebase.html#writing-tests) if fixing a bug or adding a new feature
- [x] All [code checks passed](https://pandas.pydata.org/pandas-docs/dev/development/contributing_codebase.html#pre-commit).
- [x] Added [type annotations](https://pandas.pydata.org/pandas-docs/dev/development/contributing_codebase.html#type-hints) to new arguments/methods/functions.
- [x] Added an entry in the latest `doc/source/whatsnew/vX.X.X.rst` file if fixing a bug or adding a new feature.


Problem statement:
Add `index` param to `to_dict` like in `to_json`.
`df.to_json(orient='split', index=False)` is really nice. `df.to_dict(orient='split')` basically does the same thing, but I get a dictionary back instead of a string. For symmetry, I'd like `to_dict` to also have an `index` param, so I can keep from having to `del dictionary['index']` to make them really similar.

Hints from issue discussion:
take
> but I get a dictionary back instead of a string

I'm not sure I follow what the expectation is here. But in any case, I'm +1 in adding index for API consistency with other `DataFrame.to_*` methods.

Commit message: Merge remote-tracking branch 'upstream/main' into enh-to-dict-index

Patch to review:
diff --git a/doc/source/whatsnew/v1.5.0.rst b/doc/source/whatsnew/v1.5.0.rst
index d8a319da2065e..f766884c79f0a 100644
--- a/doc/source/whatsnew/v1.5.0.rst
+++ b/doc/source/whatsnew/v1.5.0.rst
@@ -333,6 +333,7 @@ Other enhancements
 - :meth:`DataFrame.set_index` now supports a ``copy`` keyword. If ``False``, the underlying data is not copied when a new :class:`DataFrame` is returned (:issue:`48043`)
 - The method :meth:`.ExtensionArray.factorize` accepts ``use_na_sentinel=False`` for determining how null values are to be treated (:issue:`46601`)
 - The ``Dockerfile`` now installs a dedicated ``pandas-dev`` virtual environment for pandas development instead of using the ``base`` environment (:issue:`48427`)
+- Added ``index`` parameter to :meth:`DataFrame.to_dict` (:issue:`46398`)
 
 .. ---------------------------------------------------------------------------
 .. _whatsnew_150.notable_bug_fixes:
diff --git a/pandas/core/frame.py b/pandas/core/frame.py
index e471e7efb20ae..e5563855ac47f 100644
--- a/pandas/core/frame.py
+++ b/pandas/core/frame.py
@@ -1864,6 +1864,7 @@ def to_dict(
             "dict", "list", "series", "split", "tight", "records", "index"
         ] = "dict",
         into: type[dict] = dict,
+        index: bool = True,
     ) -> dict | list[dict]:
         """
         Convert the DataFrame to a dictionary.
@@ -1900,6 +1901,13 @@ def to_dict(
             instance of the mapping type you want.  If you want a
             collections.defaultdict, you must pass it initialized.
 
+        index : bool, default True
+            Whether to include the index item (and index_names item if `orient`
+            is 'tight') in the returned dictionary. Can only be ``False``
+            when `orient` is 'split' or 'tight'.
+
+            .. versionadded:: 1.5.0
+
         Returns
         -------
         dict, list or collections.abc.Mapping
@@ -2005,6 +2013,11 @@ def to_dict(
             elif orient.startswith("i"):
                 orient = "index"
 
+        if not index and orient not in ["split", "tight"]:
+            raise ValueError(
+                "'index=False' is only valid when 'orient' is 'split' or 'tight'"
+            )
+
         if orient == "dict":
             return into_c((k, v.to_dict(into)) for k, v in self.items())
 
@@ -2014,36 +2027,65 @@ def to_dict(
             )
 
         elif orient == "split":
-            return into_c(
-                (
-                    ("index", self.index.tolist()),
-                    ("columns", self.columns.tolist()),
+            if index:
+                return into_c(
                     (
-                        "data",
-                        [
-                            list(map(maybe_box_native, t))
-                            for t in self.itertuples(index=False, name=None)
-                        ],
-                    ),
+                        ("index", self.index.tolist()),
+                        ("columns", self.columns.tolist()),
+                        (
+                            "data",
+                            [
+                                list(map(maybe_box_native, t))
+                                for t in self.itertuples(index=False, name=None)
+                            ],
+                        ),
+                    )
+                )
+            else:
+                return into_c(
+                    (
+                        ("columns", self.columns.tolist()),
+                        (
+                            "data",
+                            [
+                                list(map(maybe_box_native, t))
+                                for t in self.itertuples(index=False, name=None)
+                            ],
+                        ),
+                    )
                 )
-            )
 
         elif orient == "tight":
-            return into_c(
-                (
-                    ("index", self.index.tolist()),
-                    ("columns", self.columns.tolist()),
+            if index:
+                return into_c(
                     (
-                        "data",
-                        [
-                            list(map(maybe_box_native, t))
-                            for t in self.itertuples(index=False, name=None)
-                        ],
-                    ),
-                    ("index_names", list(self.index.names)),
-                    ("column_names", list(self.columns.names)),
+                        ("index", self.index.tolist()),
+                        ("columns", self.columns.tolist()),
+                        (
+                            "data",
+                            [
+                                list(map(maybe_box_native, t))
+                                for t in self.itertuples(index=False, name=None)
+                            ],
+                        ),
+                        ("index_names", list(self.index.names)),
+                        ("column_names", list(self.columns.names)),
+                    )
+                )
+            else:
+                return into_c(
+                    (
+                        ("columns", self.columns.tolist()),
+                        (
+                            "data",
+                            [
+                                list(map(maybe_box_native, t))
+                                for t in self.itertuples(index=False, name=None)
+                            ],
+                        ),
+                        ("column_names", list(self.columns.names)),
+                    )
                 )
-            )
 
         elif orient == "series":
             return into_c((k, v) for k, v in self.items())
diff --git a/pandas/tests/frame/methods/test_to_dict.py b/pandas/tests/frame/methods/test_to_dict.py
index 6d5c32cae7368..613f7147a4a7d 100644
--- a/pandas/tests/frame/methods/test_to_dict.py
+++ b/pandas/tests/frame/methods/test_to_dict.py
@@ -421,3 +421,31 @@ def test_to_dict_returns_native_types(self, orient, data, expected_types):
         for i, key, value in assertion_iterator:
             assert value == data[key][i]
             assert type(value) is expected_types[key][i]
+
+    @pytest.mark.parametrize("orient", ["dict", "list", "series", "records", "index"])
+    def test_to_dict_index_false_error(self, orient):
+        # GH#46398
+        df = DataFrame({"col1": [1, 2], "col2": [3, 4]}, index=["row1", "row2"])
+        msg = "'index=False' is only valid when 'orient' is 'split' or 'tight'"
+        with pytest.raises(ValueError, match=msg):
+            df.to_dict(orient=orient, index=False)
+
+    @pytest.mark.parametrize(
+        "orient, expected",
+        [
+            ("split", {"columns": ["col1", "col2"], "data": [[1, 3], [2, 4]]}),
+            (
+                "tight",
+                {
+                    "columns": ["col1", "col2"],
+                    "data": [[1, 3], [2, 4]],
+                    "column_names": [None],
+                },
+            ),
+        ],
+    )
+    def test_to_dict_index_false(self, orient, expected):
+        # GH#46398
+        df = DataFrame({"col1": [1, 2], "col2": [3, 4]}, index=["row1", "row2"])
+        result = df.to_dict(orient=orient, index=False)
+        tm.assert_dict_equal(result, expected)


Code review comments:
diff_hunk: @@ -2014,36 +2027,65 @@ def to_dict(
             )
 
         elif orient == "split":
-            return into_c(
-                (
-                    ("index", self.index.tolist()),
-                    ("columns", self.columns.tolist()),
+            if index:
+                return into_c(
                     (
-                        "data",
-                        [
-                            list(map(maybe_box_native, t))
-                            for t in self.itertuples(index=False, name=None)
-                        ],
-                    ),
+                        ("index", self.index.tolist()),
+                        ("columns", self.columns.tolist()),
+                        (
+                            "data",
+                            [
+                                list(map(maybe_box_native, t))
+                                for t in self.itertuples(index=False, name=None)
+                            ],
+                        ),
+                    )
+                )
+            else:
+                return into_c(
+                    (
+                        ("columns", self.columns.tolist()),
+                        (
+                            "data",
+                            [
+                                list(map(maybe_box_native, t))
+                                for t in self.itertuples(index=False, name=None)
+                            ],
+                        ),
path: pandas/core/frame.py
text: @user1:
Sorry the diff is a bit difficult to read with the change of indentation, but if I'm not wrong, this is duplicating all this code, and I think it can be easily prevented using this pattern:
```
(('index',) if index else ()) + ('columns', 'data')
```

Does this make sense to you?

@author:
Yes, the diff on github looks more messy than on vscode. I've applied the change.

diff_hunk: @@ -1900,6 +1901,13 @@ def to_dict(
             instance of the mapping type you want.  If you want a
             collections.defaultdict, you must pass it initialized.
 
+        index : bool, default True
+            Whether to include the index item (and index_names item if `orient`
+            is 'tight') in the returned dictionary. Can only be ``False``
+            when `orient` is 'split' or 'tight'.
+
+            .. versionadded:: 1.5.0
path: pandas/core/frame.py
text: @user1:
This will be added in 1.6.0 (probably 2.0). Can you change this to 1.6 and move the whatsnew note please?

@author:
Done.