Repository: pandas-dev/pandas
Pull request: ENH: Add MultiIndex.dtypes
- [x] closes #37062 
- [x] tests added / passed
- [x] passes `black pandas`
- [x] passes `git diff upstream/master -u -- "*.py" | flake8 --diff`
- [x] whatsnew entry

Adds the .dtypes property to MultiIndex using the suggested implementation

Problem statement:
ENH: MultiIndex.dtypes
create a mi
```
In [13]: mi = pd.MultiIndex.from_product([[1,2,3], list('abc'), pd.date_range('20200101', periods=2, tz='UTC')], names=['int', 'string', 'dt'])                            
```

You can already get .dtypes, but is slightly cumbersome. I would propose adding `Multidex.dtypes` (we *already* have MultiIndex.dtype but its *always* object). I think this is worth the convenience api.
```
In [14]: mi.to_frame().dtypes                                                                                                                                              
Out[14]: 
int                     int64
string                 object
dt        datetime64[ns, UTC]
dtype: object

# we would actually implement like this.
In [15]: pd.Series({l.name: l.dtype for l in mi.levels})                                                                                                                   
Out[15]: 
int                     int64
string                 object
dt        datetime64[ns, UTC]
dtype: object
```

Hints from issue discussion:

Commit message: Add whatsnew, test and run black

Patch to review:
diff --git a/doc/source/whatsnew/v1.2.0.rst b/doc/source/whatsnew/v1.2.0.rst
index d08e8e009811a..3180b9546815f 100644
--- a/doc/source/whatsnew/v1.2.0.rst
+++ b/doc/source/whatsnew/v1.2.0.rst
@@ -191,6 +191,7 @@ Other enhancements
 - :meth:`DatetimeIndex.searchsorted`, :meth:`TimedeltaIndex.searchsorted`, :meth:`PeriodIndex.searchsorted`, and :meth:`Series.searchsorted` with datetimelike dtypes will now try to cast string arguments (listlike and scalar) to the matching datetimelike type (:issue:`36346`)
 - Added methods :meth:`IntegerArray.prod`, :meth:`IntegerArray.min`, and :meth:`IntegerArray.max` (:issue:`33790`)
 - Where possible :meth:`RangeIndex.difference` and :meth:`RangeIndex.symmetric_difference` will return :class:`RangeIndex` instead of :class:`Int64Index` (:issue:`36564`)
+- Added :meth:`MultiIndex.dtypes` (:issue:`37062`)
 
 .. _whatsnew_120.api_breaking.python:
 
diff --git a/pandas/core/indexes/multi.py b/pandas/core/indexes/multi.py
index 41f046a7f5f8a..bbe90bac4fbdf 100644
--- a/pandas/core/indexes/multi.py
+++ b/pandas/core/indexes/multi.py
@@ -699,6 +699,15 @@ def array(self):
             "'MultiIndex.to_numpy()' to get a NumPy array of tuples."
         )
 
+    @cache_readonly
+    def dtypes(self):
+        """
+        Return the dtypes as a Series for the underlying MultiIndex
+        """
+        from pandas import Series
+
+        return Series({l.name: l.dtype for l in self.levels})
+
     @property
     def shape(self):
         """
diff --git a/pandas/tests/indexes/multi/test_get_set.py b/pandas/tests/indexes/multi/test_get_set.py
index b9132f429905d..3f6f26536aa9c 100644
--- a/pandas/tests/indexes/multi/test_get_set.py
+++ b/pandas/tests/indexes/multi/test_get_set.py
@@ -27,6 +27,11 @@ def test_get_level_number_integer(idx):
         idx._get_level_number("fourth")
 
 
+def test_get_dtypes(idx):
+    expected = pd.Series({"first": np.dtype("O"), "second": np.dtype("O")})
+    assert expected.equals(idx.dtypes)
+
+
 def test_set_name_methods(idx, index_names):
     # so long as these are synonyms, we don't need to test set_names
     assert idx.rename == idx.set_names


Code review comments:
diff_hunk: @@ -27,6 +27,11 @@ def test_get_level_number_integer(idx):
         idx._get_level_number("fourth")
 
 
+def test_get_dtypes(idx):
path: pandas/tests/indexes/multi/test_get_set.py
text: @user1:
can you add the issue number as a comment

i think idx is too simple, can you construct a MI like in the OP and test it here