Repository: pandas-dev/pandas
Pull request: ENH:Add EA types to read CSV
Closes GH23228

- [x ] closes #23228
- [ x] tests added / passed
- [x ] passes `git diff upstream/master -u -- "*.py" | flake8 --diff`
- [x ] whatsnew - May need some  guidance on this part.


Problem statement:
ENH: support EA types in read_csv
```
In [3]: df = pd.DataFrame({'Int': pd.Series([1, 2, 3], dtype='Int64'), 'A': [1, 2, 1]})
   ...: df
   ...: 
Out[3]: 
  Int  A
0   1  1
1   2  2
2   3  1

In [4]: data = df.to_csv(index=False)

In [5]: data
Out[5]: 'Int,A\n1,1\n2,2\n3,1\n'

In [6]: from io import StringIO

In [7]: pd.read_csv(StringIO(data), dtype={'Int': 'Int64'})~/pandas/pandas/_libs/parsers.pyx in pandas._libs.parsers.TextReader._read_rows()
    968 
    969         self._start_clock()
--> 970         columns = self._convert_column_data(rows=rows,
    971                                             footer=footer,
    972                                             upcast_na=True)

~/pandas/pandas/_libs/parsers.pyx in pandas._libs.parsers.TextReader._convert_column_data()
   1096 
   1097             # Should return as the desired dtype (inferred or specified)
-> 1098             col_res, na_count = self._convert_tokens(
   1099                 i, start, end, name, na_filter, na_hashset,
   1100                 na_flist, col_dtype)

~/pandas/pandas/_libs/parsers.pyx in pandas._libs.parsers.TextReader._convert_tokens()
   1121 
   1122         if col_dtype is not None:
-> 1123             col_res, na_count = self._convert_with_dtype(
   1124                 col_dtype, i, start, end, na_filter,
   1125                 1, na_hashset, na_flist)

~/pandas/pandas/_libs/parsers.pyx in pandas._libs.parsers.TextReader._convert_with_dtype()
   1249                             "using parse_dates instead".format(dtype=dtype))
   1250         else:
-> 1251             raise TypeError("the dtype {dtype} is not "
   1252                             "supported for parsing".format(dtype=dtype))
   1253 

TypeError: the dtype Int64 is not supported for parsing
```

we already support Categorical, would be nice to have a general interface to this

Parsing with the actual dtype is also broken (as it parses to object)
```
In [8]: from pandas.core.arrays.integer import Int64Dtype

In [9]: pd.read_csv(StringIO(data), dtype={'Int': Int64Dtype})
Out[9]: 
  Int  A
0   1  1
1   2  2
2   3  1

In [10]: pd.read_csv(StringIO(data), dtype={'Int': Int64Dtype}).dtypes
Out[10]: 
Int    object
A       int64
dtype: object
```

Hints from issue discussion:
I think I have a fix for the fix part of this issue. 

I will create a PR and continue working on the second part of the issue.

Commit message: Correct tests to match existing patterns, address merge comments

Ensure that dtype is always an initiated object

Patch to review:
diff --git a/doc/source/whatsnew/v0.24.0.rst b/doc/source/whatsnew/v0.24.0.rst
index a84fd118061bc..8f8f6cac8bad9 100644
--- a/doc/source/whatsnew/v0.24.0.rst
+++ b/doc/source/whatsnew/v0.24.0.rst
@@ -31,7 +31,10 @@ New features
 - :func:`read_feather` now accepts ``columns`` as an argument, allowing the user to specify which columns should be read. (:issue:`24025`)
 - :func:`DataFrame.to_html` now accepts ``render_links`` as an argument, allowing the user to generate HTML with links to any URLs that appear in the DataFrame.
   See the :ref:`section on writing HTML <io.html>` in the IO docs for example usage. (:issue:`2679`)
+- :func:`pandas.read_csv` now supports pandas extension types as an argument to ``dtype``, allowing the user to use pandas extension types when reading CSVs. (:issue:`23228`)
 - :meth:`DataFrame.shift` :meth:`Series.shift`, :meth:`ExtensionArray.shift`, :meth:`SparseArray.shift`, :meth:`Period.shift`, :meth:`GroupBy.shift`, :meth:`Categorical.shift`, :meth:`NDFrame.shift` and :meth:`Block.shift` now accept `fill_value` as an argument, allowing the user to specify a value which will be used instead of NA/NaT in the empty periods. (:issue:`15486`)
+- :func:`pandas.read_csv` now supports ``EA`` types as an argument to ``dtype``,
+  allowing the user to use ``EA`` types when reading CSVs. (:issue:`23228`)
 
 .. _whatsnew_0240.values_api:
 
diff --git a/pandas/_libs/parsers.pyx b/pandas/_libs/parsers.pyx
index 169aa4ffe72da..e7e6b36b7e5ef 100644
--- a/pandas/_libs/parsers.pyx
+++ b/pandas/_libs/parsers.pyx
@@ -50,7 +50,7 @@ from pandas.core.dtypes.common import (
     is_integer_dtype, is_float_dtype,
     is_bool_dtype, is_object_dtype,
     is_datetime64_dtype,
-    pandas_dtype)
+    pandas_dtype, is_extension_array_dtype)
 from pandas.core.arrays import Categorical
 from pandas.core.dtypes.concat import union_categoricals
 import pandas.io.common as icom
@@ -983,7 +983,6 @@ cdef class TextReader:
                                             footer=footer,
                                             upcast_na=True)
         self._end_clock('Type conversion')
-
         self._start_clock()
         if len(columns) > 0:
             rows_read = len(list(columns.values())[0])
@@ -1123,7 +1122,9 @@ cdef class TextReader:
                 if na_filter:
                     self._free_na_set(na_hashset)
 
-            if upcast_na and na_count > 0:
+            try_upcast = upcast_na and na_count > 0
+            # don't try to upcast EAs
+            if try_upcast and not is_extension_array_dtype(col_dtype):
                 col_res = _maybe_upcast(col_res)
 
             if col_res is None:
@@ -1215,6 +1216,22 @@ cdef class TextReader:
                 cats, codes, dtype, true_values=true_values)
             return cat, na_count
 
+        elif is_extension_array_dtype(dtype):
+            result, na_count = self._string_convert(i, start, end, na_filter,
+                                                    na_hashset)
+            array_type = dtype.construct_array_type()
+            try:
+                # use _from_sequence_of_strings if the class defines it
+                result = array_type._from_sequence_of_strings(result,
+                                                              dtype=dtype)
+            except NotImplementedError:
+                raise NotImplementedError(
+                    "Extension Array: {ea} must implement "
+                    "_from_sequence_of_strings in order "
+                    "to be used in parser methods".format(ea=array_type))
+
+            return result, na_count
+
         elif is_integer_dtype(dtype):
             try:
                 result, na_count = _try_int64(self.parser, i, start,
@@ -1240,7 +1257,6 @@ cdef class TextReader:
             if result is not None and dtype != 'float64':
                 result = result.astype(dtype)
             return result, na_count
-
         elif is_bool_dtype(dtype):
             result, na_count = _try_bool_flex(self.parser, i, start, end,
                                               na_filter, na_hashset,
@@ -2173,7 +2189,11 @@ def _concatenate_chunks(list chunks):
             result[name] = union_categoricals(arrs,
                                               sort_categories=sort_categories)
         else:
-            result[name] = np.concatenate(arrs)
+            if is_extension_array_dtype(dtype):
+                array_type = dtype.construct_array_type()
+                result[name] = array_type._concat_same_type(arrs)
+            else:
+                result[name] = np.concatenate(arrs)
 
     if warning_columns:
         warning_names = ','.join(warning_columns)
diff --git a/pandas/core/arrays/base.py b/pandas/core/arrays/base.py
index 2d4f8ca9c2cee..1b46d9e3c2c48 100644
--- a/pandas/core/arrays/base.py
+++ b/pandas/core/arrays/base.py
@@ -128,6 +128,30 @@ def _from_sequence(cls, scalars, dtype=None, copy=False):
         """
         raise AbstractMethodError(cls)
 
+    @classmethod
+    def _from_sequence_of_strings(cls, strings, dtype=None, copy=False):
+        """Construct a new ExtensionArray from a sequence of strings.
+
+        .. versionadded:: 0.24.0
+
+        Parameters
+        ----------
+        strings : Sequence
+            Each element will be an instance of the scalar type for this
+            array, ``cls.dtype.type``.
+        dtype : dtype, optional
+            Construct for this particular dtype. This should be a Dtype
+            compatible with the ExtensionArray.
+        copy : boolean, default False
+            If True, copy the underlying data.
+
+        Returns
+        -------
+        ExtensionArray
+
+        """
+        raise AbstractMethodError(cls)
+
     @classmethod
     def _from_factorized(cls, values, original):
         """
diff --git a/pandas/core/arrays/integer.py b/pandas/core/arrays/integer.py
index 38dc68e8f77a3..eaec76b96a24d 100644
--- a/pandas/core/arrays/integer.py
+++ b/pandas/core/arrays/integer.py
@@ -19,6 +19,7 @@
 
 from pandas.core import nanops
 from pandas.core.arrays import ExtensionArray, ExtensionOpsMixin
+from pandas.core.tools.numeric import to_numeric
 
 
 class _IntegerDtype(ExtensionDtype):
@@ -261,6 +262,11 @@ def __init__(self, values, mask, copy=False):
     def _from_sequence(cls, scalars, dtype=None, copy=False):
         return integer_array(scalars, dtype=dtype, copy=copy)
 
+    @classmethod
+    def _from_sequence_of_strings(cls, strings, dtype=None, copy=False):
+        scalars = to_numeric(strings, errors="raise")
+        return cls._from_sequence(scalars, dtype, copy)
+
     @classmethod
     def _from_factorized(cls, values, original):
         return integer_array(values, dtype=original.dtype)
diff --git a/pandas/core/dtypes/common.py b/pandas/core/dtypes/common.py
index e1141c6b6b3a8..b67f57e4aeee3 100644
--- a/pandas/core/dtypes/common.py
+++ b/pandas/core/dtypes/common.py
@@ -1986,6 +1986,9 @@ def pandas_dtype(dtype):
     # registered extension types
     result = _pandas_registry.find(dtype) or registry.find(dtype)
     if result is not None:
+        # ensure result is an instantiated type
+        if isinstance(result, type):
+            return result()
         return result
 
     # un-registered extension types
diff --git a/pandas/core/indexes/numeric.py b/pandas/core/indexes/numeric.py
index 9d6a56200df6e..445cadfa18dfc 100644
--- a/pandas/core/indexes/numeric.py
+++ b/pandas/core/indexes/numeric.py
@@ -7,8 +7,8 @@
 from pandas.util._decorators import Appender, cache_readonly
 
 from pandas.core.dtypes.common import (
-    is_bool, is_bool_dtype, is_dtype_equal, is_float, is_integer_dtype,
-    is_scalar, needs_i8_conversion, pandas_dtype)
+    is_bool, is_bool_dtype, is_dtype_equal, is_float,
+    is_integer_dtype, is_scalar, needs_i8_conversion, pandas_dtype)
 import pandas.core.dtypes.concat as _concat
 from pandas.core.dtypes.missing import isna
 
diff --git a/pandas/io/parsers.py b/pandas/io/parsers.py
index de0ed9407e161..74bdd25f7090b 100755
--- a/pandas/io/parsers.py
+++ b/pandas/io/parsers.py
@@ -28,8 +28,8 @@
 from pandas.core.dtypes.cast import astype_nansafe
 from pandas.core.dtypes.common import (
     ensure_object, is_bool_dtype, is_categorical_dtype, is_dtype_equal,
-    is_float, is_integer, is_integer_dtype, is_list_like, is_object_dtype,
-    is_scalar, is_string_dtype)
+    is_extension_array_dtype, is_float, is_integer, is_integer_dtype,
+    is_list_like, is_object_dtype, is_scalar, is_string_dtype, pandas_dtype)
 from pandas.core.dtypes.dtypes import CategoricalDtype
 from pandas.core.dtypes.missing import isna
 
@@ -1659,16 +1659,20 @@ def _convert_to_ndarrays(self, dct, na_values, na_fvalues, verbose=False,
                     values, set(col_na_values) | col_na_fvalues,
                     try_num_bool=False)
             else:
+                is_str_or_ea_dtype = (is_string_dtype(cast_type)
+                                      or is_extension_array_dtype(cast_type))
                 # skip inference if specified dtype is object
-                try_num_bool = not (cast_type and is_string_dtype(cast_type))
+                # or casting to an EA
+                try_num_bool = not (cast_type and is_str_or_ea_dtype)
 
                 # general type inference and conversion
                 cvals, na_count = self._infer_types(
                     values, set(col_na_values) | col_na_fvalues,
                     try_num_bool)
 
-                # type specified in dtype param
-                if cast_type and not is_dtype_equal(cvals, cast_type):
+                # type specified in dtype param or cast_type is an EA
+                if cast_type and (not is_dtype_equal(cvals, cast_type)
+                                  or is_extension_array_dtype(cast_type)):
                     try:
                         if (is_bool_dtype(cast_type) and
                                 not is_categorical_dtype(cast_type)
@@ -1765,6 +1769,23 @@ def _cast_types(self, values, cast_type, column):
                 cats, cats.get_indexer(values), cast_type,
                 true_values=self.true_values)
 
+        # use the EA's implementation of casting
+        elif is_extension_array_dtype(cast_type):
+            try:
+                array_type = cast_type.construct_array_type()
+            except AttributeError:
+                cast_type = pandas_dtype(cast_type)
+                array_type = cast_type.construct_array_type()
+
+            try:
+                return array_type._from_sequence_of_strings(values,
+                                                            dtype=cast_type)
+            except NotImplementedError:
+                raise NotImplementedError(
+                    "Extension Array: {ea} must implement "
+                    "_from_sequence_of_strings in order "
+                    "to be used in parser methods".format(ea=array_type))
+
         else:
             try:
                 values = astype_nansafe(values, cast_type,
@@ -2174,7 +2195,14 @@ def __init__(self, f, **kwds):
 
         self.verbose = kwds['verbose']
         self.converters = kwds['converters']
-        self.dtype = kwds['dtype']
+
+        # convert dtype to a pandas_dtype
+        dtype = kwds['dtype']
+        if isinstance(dtype, dict):
+            self.dtype = {k: pandas_dtype(dtype[k])
+                          for k in dtype}
+        else:
+            self.dtype = dtype
 
         self.thousands = kwds['thousands']
         self.decimal = kwds['decimal']
diff --git a/pandas/tests/extension/base/__init__.py b/pandas/tests/extension/base/__init__.py
index 57704b77bb233..1f7ee2ae17e4a 100644
--- a/pandas/tests/extension/base/__init__.py
+++ b/pandas/tests/extension/base/__init__.py
@@ -53,3 +53,4 @@ class TestMyDtype(BaseDtypeTests):
 from .missing import BaseMissingTests  # noqa
 from .reshaping import BaseReshapingTests  # noqa
 from .setitem import BaseSetitemTests  # noqa
+from .io import BaseParsingTests  # noqa
diff --git a/pandas/tests/extension/base/io.py b/pandas/tests/extension/base/io.py
new file mode 100644
index 0000000000000..41417b086fc9a
--- /dev/null
+++ b/pandas/tests/extension/base/io.py
@@ -0,0 +1,24 @@
+import numpy as np
+import pytest
+
+from pandas.compat import StringIO
+
+import pandas as pd
+import pandas.testing as tm
+
+from .base import BaseExtensionTests
+
+
+class BaseParsingTests(BaseExtensionTests):
+
+    @pytest.mark.parametrize('engine', ['c', 'python'])
+    def test_EA_types(self, engine, data):
+        df = pd.DataFrame({
+            'with_dtype': pd.Series(data, dtype=str(data.dtype))
+        })
+        csv_output = df.to_csv(index=False, na_rep=np.nan)
+        result = pd.read_csv(StringIO(csv_output), dtype={
+            'with_dtype': str(data.dtype)
+        }, engine=engine)
+        assert result is not None
+        tm.assert_frame_equal(df, result)
diff --git a/pandas/tests/extension/decimal/array.py b/pandas/tests/extension/decimal/array.py
index 7e618dfd2b92e..1823eeb4d7fc0 100644
--- a/pandas/tests/extension/decimal/array.py
+++ b/pandas/tests/extension/decimal/array.py
@@ -75,6 +75,11 @@ def dtype(self):
     def _from_sequence(cls, scalars, dtype=None, copy=False):
         return cls(scalars)
 
+    @classmethod
+    def _from_sequence_of_strings(cls, strings, dtype=None, copy=False):
+        return cls._from_sequence([decimal.Decimal(x) for x in strings],
+                                  dtype, copy)
+
     @classmethod
     def _from_factorized(cls, values, original):
         return cls(values)
diff --git a/pandas/tests/extension/test_categorical.py b/pandas/tests/extension/test_categorical.py
index c876db416470c..ac52d8f15b8ce 100644
--- a/pandas/tests/extension/test_categorical.py
+++ b/pandas/tests/extension/test_categorical.py
@@ -237,3 +237,7 @@ def _compare_other(self, s, data, op_name, other):
         else:
             with pytest.raises(TypeError):
                 op(data, other)
+
+
+class TestParsing(base.BaseParsingTests):
+    pass
diff --git a/pandas/tests/extension/test_integer.py b/pandas/tests/extension/test_integer.py
index e21ca81bcf5c3..aadf9f2f12b68 100644
--- a/pandas/tests/extension/test_integer.py
+++ b/pandas/tests/extension/test_integer.py
@@ -218,3 +218,7 @@ class TestBooleanReduce(base.BaseBooleanReduceTests):
 
 class TestPrinting(base.BasePrintingTests):
     pass
+
+
+class TestParsing(base.BaseParsingTests):
+    pass
diff --git a/pandas/tests/extension/test_interval.py b/pandas/tests/extension/test_interval.py
index 644f3ef94f40b..6eedbfb4aba39 100644
--- a/pandas/tests/extension/test_interval.py
+++ b/pandas/tests/extension/test_interval.py
@@ -152,3 +152,11 @@ class TestPrinting(BaseInterval, base.BasePrintingTests):
     @pytest.mark.skip(reason="custom repr")
     def test_array_repr(self, data, size):
         pass
+
+
+class TestParsing(BaseInterval, base.BaseParsingTests):
+    @pytest.mark.parametrize('engine', ['c', 'python'])
+    def test_EA_types(self, engine, data):
+        expected_msg = r'.*must implement _from_sequence_of_strings.*'
+        with pytest.raises(NotImplementedError, match=expected_msg):
+            super(TestParsing, self).test_EA_types(engine, data)
diff --git a/pandas/tests/extension/test_numpy.py b/pandas/tests/extension/test_numpy.py
index 70a3a8ab58aac..7ca6882c7441b 100644
--- a/pandas/tests/extension/test_numpy.py
+++ b/pandas/tests/extension/test_numpy.py
@@ -210,3 +210,7 @@ def test_concat_mixed_dtypes(self, data):
 
 class TestSetitem(BaseNumPyTests, base.BaseSetitemTests):
     pass
+
+
+class TestParsing(BaseNumPyTests, base.BaseParsingTests):
+    pass
diff --git a/pandas/tests/extension/test_period.py b/pandas/tests/extension/test_period.py
index 08e21fc30ad10..813efcb5678d3 100644
--- a/pandas/tests/extension/test_period.py
+++ b/pandas/tests/extension/test_period.py
@@ -156,3 +156,11 @@ class TestGroupby(BasePeriodTests, base.BaseGroupbyTests):
 
 class TestPrinting(BasePeriodTests, base.BasePrintingTests):
     pass
+
+
+class TestParsing(BasePeriodTests, base.BaseParsingTests):
+    @pytest.mark.parametrize('engine', ['c', 'python'])
+    def test_EA_types(self, engine, data):
+        expected_msg = r'.*must implement _from_sequence_of_strings.*'
+        with pytest.raises(NotImplementedError, match=expected_msg):
+            super(TestParsing, self).test_EA_types(engine, data)
diff --git a/pandas/tests/extension/test_sparse.py b/pandas/tests/extension/test_sparse.py
index 257eb44cd94fe..678b7cd1b6cd2 100644
--- a/pandas/tests/extension/test_sparse.py
+++ b/pandas/tests/extension/test_sparse.py
@@ -359,3 +359,12 @@ class TestPrinting(BaseSparseTests, base.BasePrintingTests):
     @pytest.mark.xfail(reason='Different repr', strict=True)
     def test_array_repr(self, data, size):
         super(TestPrinting, self).test_array_repr(data, size)
+
+
+class TestParsing(BaseSparseTests, base.BaseParsingTests):
+    @pytest.mark.parametrize('engine', ['c', 'python'])
+    def test_EA_types(self, engine, data):
+        expected_msg = r'.*must implement _from_sequence_of_strings.*'
+        with pytest.raises(NotImplementedError, match=expected_msg):
+            super(TestParsing, self).test_EA_types(engine, data)
+
diff --git a/pandas/tests/io/parser/common.py b/pandas/tests/io/parser/common.py
new file mode 100644
index 0000000000000..e69de29bb2d1d


Code review comments:
diff_hunk: @@ -31,7 +31,10 @@ New features
 - :func:`read_feather` now accepts ``columns`` as an argument, allowing the user to specify which columns should be read. (:issue:`24025`)
 - :func:`DataFrame.to_html` now accepts ``render_links`` as an argument, allowing the user to generate HTML with links to any URLs that appear in the DataFrame.
   See the :ref:`section on writing HTML <io.html>` in the IO docs for example usage. (:issue:`2679`)
+- :func:`pandas.read_csv` now supports pandas extension types as an argument to ``dtype``, allowing the user to use pandas extension types when reading CSVs. (:issue:`23228`)
 - :meth:`DataFrame.shift` :meth:`Series.shift`, :meth:`ExtensionArray.shift`, :meth:`SparseArray.shift`, :meth:`Period.shift`, :meth:`GroupBy.shift`, :meth:`Categorical.shift`, :meth:`NDFrame.shift` and :meth:`Block.shift` now accept `fill_value` as an argument, allowing the user to specify a value which will be used instead of NA/NaT in the empty periods. (:issue:`15486`)
+- :func:`pandas.read_csv` now supports ``EA`` types as an argument to ``dtype``,
+  allowing the user to use ``EA`` types when reading CSVs. (:issue:`23228`)
path: doc/source/whatsnew/v0.24.0.rst
text: @user1:
looks like the note is duplicated? the first one looks good, so remove this one

diff_hunk: @@ -1765,6 +1769,23 @@ def _cast_types(self, values, cast_type, column):
                 cats, cats.get_indexer(values), cast_type,
                 true_values=self.true_values)
 
+        # use the EA's implementation of casting
+        elif is_extension_array_dtype(cast_type):
+            try:
+                array_type = cast_type.construct_array_type()
+            except AttributeError:
+                cast_type = pandas_dtype(cast_type)
path: pandas/io/parsers.py
text: @user1:
why are you catching AttributeError here? this should *always* exist, it is a required feature of EAs (if it doesn't then we need to fix that).

What is this failing on ?

@author:
Don't need this. It was before it was in this `if` block, just didn't remove it.

diff_hunk: @@ -0,0 +1,24 @@
+import numpy as np
+import pytest
+
+from pandas.compat import StringIO
+
+import pandas as pd
+import pandas.testing as tm
+
+from .base import BaseExtensionTests
+
+
+class BaseParsingTests(BaseExtensionTests):
+
+    @pytest.mark.parametrize('engine', ['c', 'python'])
+    def test_EA_types(self, engine, data):
+        df = pd.DataFrame({
+            'with_dtype': pd.Series(data, dtype=str(data.dtype))
+        })
+        csv_output = df.to_csv(index=False, na_rep=np.nan)
+        result = pd.read_csv(StringIO(csv_output), dtype={
+            'with_dtype': str(data.dtype)
+        }, engine=engine)
+        assert result is not None
+        tm.assert_frame_equal(df, result)
path: pandas/tests/extension/base/io.py
text: @user1:
you don't need the ``assert result is not None``

```
expected = df
tm.assert_frame_equal(result, expected)
```

diff_hunk: @@ -1986,6 +1986,9 @@ def pandas_dtype(dtype):
     # registered extension types
     result = _pandas_registry.find(dtype) or registry.find(dtype)
     if result is not None:
+        # ensure result is an instantiated type
+        if isinstance(result, type):
path: pandas/core/dtypes/common.py
text: @user1:
can you explain where exactly you think this needed? this violates the return values of this function and should not be needed here. This function returns a type not an instance

@author:
To be honest, I'm not sure anymore. I took it out and it seems to be working fine. 

@user1:
ok great!

@author:
One place that this did come up is in `is_bool_dtype` though. I think there a bug there. I can open an issue about it if you want, but basically what happens is if an a EA class is passed in it will return a property method. This can create unexpected side effects. for example:

```python
if is_bool_dtype(Int64dtype):
    print('is a bool dtype')
else:
    print('not a bool dtype')
```

This will print `is a bool dtype` because `is_bool_dtype` won't return `True` or `False`, it will actually return a `property_method` reference. 

That is (one of) the reasons I thought that I needed this.