Repository: pandas-dev/pandas

Issue:
IntervalDtype inconsistencies and bugs
**1. Inconsistent comparisons versus string `'interval'`:**
```python
In [2]: IntervalDtype() == 'interval'
Out[2]: True

In [3]: IntervalDtype('interval') == 'interval'
Out[3]: False

In [4]: IntervalDtype('int64') == 'interval'
Out[4]: False
```
I'd expect all of these to return `True`, like how `CategoricalDtype(*, *) == 'category'` always returns `True`.

<br />

**2. Inconsistent comparisons versus `IntervalDtype(None)`:**

```python
In [5]: IntervalDtype(None) == IntervalDtype('interval')
Out[5]: False

In [6]: IntervalDtype(None) == IntervalDtype('int64')
Out[6]: False
```
I'd expect all of these to return `True`, like how `CDT(None, None) == CDT(*, *)` always returns `True`.

<br />

**3. `IntervalDtype.name` attribute changes**

```python
In [7]: IntervalDtype().name
Out[7]: 'interval'

In [8]: IntervalDtype('interval').name
Out[8]: 'interval[]'

In [9]: IntervalDtype('int64').name
Out[9]: 'interval[int64]'
```
`CategoricalDtype.name`  attribute is always the same:

```python
In [10]: CategoricalDtype(list('abc'), True).name
Out[10]: 'category'

In [11]: CategoricalDtype(list('wxyz'), False).name
Out[11]: 'category'
```
I'd expect `IntervalDtype.name` to always return `'interval'`, like how `CDT.name` always returns `'category'`.  This makes the code for checking equality against strings (i.e. what I described in 1) simpler.  I don't think the behavior of `str(IntervalDtype)` should change, which is currently the same as `IntervalDtype.name`, so I'd still have that return strings specifying the subtype.

<br />

**4. ~`CategoricalDtype` gets cached incorrectly:~** (No longer an issue due to #19022)
```python
In [12]: idt1 = IntervalDtype(CategoricalDtype(list('abc'), True))

In [13]: idt2 = IntervalDtype(CategoricalDtype(list('wxyz'), False))

In [14]: idt2.subtype
Out[14]: CategoricalDtype(categories=['a', 'b', 'c'], ordered=True)
```
This looks to be caused by the caching being done by string representation, and `str(CDT(*, *))` always returns `'category'`:

https://github.com/pandas-dev/pandas/blob/e1d5a2738235fec22f3cfad4814e09e3e3786f8c/pandas/core/dtypes/dtypes.py#L673-L679

Can caching be removed entirely for `IntervalDtype`, or is there some need/advantage that I'm not seeing?  Looking at the other dtypes, `CategoricalDtype` appears to have had the caching code removed, but `PeriodDtype` and `DatetimeTZDtype` are using it.
  


Discussion:
IIRC the reason for caching was:

a) perf (constructing a dtype each time involves some checks)
b) equality/hashing; the caching gives exactly the same object back so we can do is comparison. this is quite important as these are singular objects (though they are parameterized). IOW, ``IntervalDtype()`` should always be a singleton, and so on.

Resolving patch:
diff --git a/pandas/core/dtypes/dtypes.py b/pandas/core/dtypes/dtypes.py
--- a/pandas/core/dtypes/dtypes.py
+++ b/pandas/core/dtypes/dtypes.py
@@ -626,6 +626,7 @@ class IntervalDtype(ExtensionDtype):
 
     THIS IS NOT A REAL NUMPY DTYPE
     """
+    name = 'interval'
     type = IntervalDtypeType
     kind = None
     str = '|O08'
@@ -653,8 +654,8 @@ def __new__(cls, subtype=None):
             u.subtype = None
             return u
         elif (isinstance(subtype, compat.string_types) and
-              subtype == 'interval'):
-            subtype = ''
+              subtype.lower() == 'interval'):
+            subtype = None
         else:
             if isinstance(subtype, compat.string_types):
                 m = cls._match.search(subtype)
@@ -666,11 +667,6 @@ def __new__(cls, subtype=None):
             except TypeError:
                 raise ValueError("could not construct IntervalDtype")
 
-        if subtype is None:
-            u = object.__new__(cls)
-            u.subtype = None
-            return u
-
         if is_categorical_dtype(subtype) or is_string_dtype(subtype):
             # GH 19016
             msg = ('category, object, and string subtypes are not supported '
@@ -692,31 +688,29 @@ def construct_from_string(cls, string):
         if its not possible
         """
         if isinstance(string, compat.string_types):
-            try:
-                return cls(string)
-            except ValueError:
-                pass
-        raise TypeError("could not construct IntervalDtype")
+            return cls(string)
+        msg = "a string needs to be passed, got type {typ}"
+        raise TypeError(msg.format(typ=type(string)))
 
     def __unicode__(self):
         if self.subtype is None:
             return "interval"
         return "interval[{subtype}]".format(subtype=self.subtype)
 
-    @property
-    def name(self):
-        return str(self)
-
     def __hash__(self):
         # make myself hashable
         return hash(str(self))
 
     def __eq__(self, other):
         if isinstance(other, compat.string_types):
-            return other == self.name or other == self.name.title()
-
-        return (isinstance(other, IntervalDtype) and
-                self.subtype == other.subtype)
+            return other.lower() in (self.name.lower(), str(self).lower())
+        elif not isinstance(other, IntervalDtype):
+            return False
+        elif self.subtype is None or other.subtype is None:
+            # None should match any subtype
+            return True
+        else:
+            return self.subtype == other.subtype
 
     @classmethod
     def is_dtype(cls, dtype):


Test patch:
diff --git a/pandas/tests/dtypes/test_dtypes.py b/pandas/tests/dtypes/test_dtypes.py
--- a/pandas/tests/dtypes/test_dtypes.py
+++ b/pandas/tests/dtypes/test_dtypes.py
@@ -433,7 +433,7 @@ def test_hash_vs_equality(self):
         assert dtype2 == dtype
         assert dtype3 == dtype
         assert dtype is dtype2
-        assert dtype2 is dtype
+        assert dtype2 is dtype3
         assert dtype3 is dtype
         assert hash(dtype) == hash(dtype2)
         assert hash(dtype) == hash(dtype3)
@@ -451,14 +451,19 @@ def test_hash_vs_equality(self):
         assert hash(dtype2) == hash(dtype2)
         assert hash(dtype2) == hash(dtype3)
 
-    def test_construction(self):
-        with pytest.raises(ValueError):
-            IntervalDtype('xx')
+    @pytest.mark.parametrize('subtype', [
+        'interval[int64]', 'Interval[int64]', 'int64', np.dtype('int64')])
+    def test_construction(self, subtype):
+        i = IntervalDtype(subtype)
+        assert i.subtype == np.dtype('int64')
+        assert is_interval_dtype(i)
 
-        for s in ['interval[int64]', 'Interval[int64]', 'int64']:
-            i = IntervalDtype(s)
-            assert i.subtype == np.dtype('int64')
-            assert is_interval_dtype(i)
+    @pytest.mark.parametrize('subtype', [None, 'interval', 'Interval'])
+    def test_construction_generic(self, subtype):
+        # generic
+        i = IntervalDtype(subtype)
+        assert i.subtype is None
+        assert is_interval_dtype(i)
 
     @pytest.mark.parametrize('subtype', [
         CategoricalDtype(list('abc'), False),
@@ -471,17 +476,27 @@ def test_construction_not_supported(self, subtype):
         with tm.assert_raises_regex(TypeError, msg):
             IntervalDtype(subtype)
 
-    def test_construction_generic(self):
-        # generic
-        i = IntervalDtype('interval')
-        assert i.subtype == ''
-        assert is_interval_dtype(i)
-        assert str(i) == 'interval[]'
+    def test_construction_errors(self):
+        msg = 'could not construct IntervalDtype'
+        with tm.assert_raises_regex(ValueError, msg):
+            IntervalDtype('xx')
 
-        i = IntervalDtype()
-        assert i.subtype is None
-        assert is_interval_dtype(i)
-        assert str(i) == 'interval'
+    def test_construction_from_string(self):
+        result = IntervalDtype('interval[int64]')
+        assert is_dtype_equal(self.dtype, result)
+        result = IntervalDtype.construct_from_string('interval[int64]')
+        assert is_dtype_equal(self.dtype, result)
+
+    @pytest.mark.parametrize('string', [
+        'foo', 'interval[foo]', 'foo[int64]', 0, 3.14, ('a', 'b'), None])
+    def test_construction_from_string_errors(self, string):
+        if isinstance(string, string_types):
+            error, msg = ValueError, 'could not construct IntervalDtype'
+        else:
+            error, msg = TypeError, 'a string needs to be passed, got type'
+
+        with tm.assert_raises_regex(error, msg):
+            IntervalDtype.construct_from_string(string)
 
     def test_subclass(self):
         a = IntervalDtype('interval[int64]')
@@ -506,36 +521,45 @@ def test_is_dtype(self):
         assert not IntervalDtype.is_dtype(np.int64)
         assert not IntervalDtype.is_dtype(np.float64)
 
-    def test_identity(self):
-        assert (IntervalDtype('interval[int64]') ==
-                IntervalDtype('interval[int64]'))
-
     def test_coerce_to_dtype(self):
         assert (_coerce_to_dtype('interval[int64]') ==
                 IntervalDtype('interval[int64]'))
 
-    def test_construction_from_string(self):
-        result = IntervalDtype('interval[int64]')
-        assert is_dtype_equal(self.dtype, result)
-        result = IntervalDtype.construct_from_string('interval[int64]')
-        assert is_dtype_equal(self.dtype, result)
-        with pytest.raises(TypeError):
-            IntervalDtype.construct_from_string('foo')
-        with pytest.raises(TypeError):
-            IntervalDtype.construct_from_string('interval[foo]')
-        with pytest.raises(TypeError):
-            IntervalDtype.construct_from_string('foo[int64]')
-
     def test_equality(self):
         assert is_dtype_equal(self.dtype, 'interval[int64]')
         assert is_dtype_equal(self.dtype, IntervalDtype('int64'))
-        assert is_dtype_equal(self.dtype, IntervalDtype('int64'))
         assert is_dtype_equal(IntervalDtype('int64'), IntervalDtype('int64'))
 
         assert not is_dtype_equal(self.dtype, 'int64')
         assert not is_dtype_equal(IntervalDtype('int64'),
                                   IntervalDtype('float64'))
 
+    @pytest.mark.parametrize('subtype', [
+        None, 'interval', 'Interval', 'int64', 'uint64', 'float64',
+        'complex128', 'datetime64', 'timedelta64', PeriodDtype('Q')])
+    def test_equality_generic(self, subtype):
+        # GH 18980
+        dtype = IntervalDtype(subtype)
+        assert is_dtype_equal(dtype, 'interval')
+        assert is_dtype_equal(dtype, IntervalDtype())
+
+    @pytest.mark.parametrize('subtype', [
+        'int64', 'uint64', 'float64', 'complex128', 'datetime64',
+        'timedelta64', PeriodDtype('Q')])
+    def test_name_repr(self, subtype):
+        # GH 18980
+        dtype = IntervalDtype(subtype)
+        expected = 'interval[{subtype}]'.format(subtype=subtype)
+        assert str(dtype) == expected
+        assert dtype.name == 'interval'
+
+    @pytest.mark.parametrize('subtype', [None, 'interval', 'Interval'])
+    def test_name_repr_generic(self, subtype):
+        # GH 18980
+        dtype = IntervalDtype(subtype)
+        assert str(dtype) == 'interval'
+        assert dtype.name == 'interval'
+
     def test_basic(self):
         assert is_interval_dtype(self.dtype)