Repository: pandas-dev/pandas

Issue:
Inconsistent isin with strings
```
In [5]: s = Series(list('abc'))

In [6]: s.isin('a')
Out[6]:
0     True
1    False
2    False
dtype: bool

In [5]: s = Series(list('abc'))

In [6]: s.isin('a')
Out[6]:
0     True
1    False
2    False
dtype: bool

In [7]: s = Series(['aaa', 'b', 'c'])

In [8]: s.isin('aaa')
Out[8]:
0    False
1    False
2    False
dtype: bool
```

I would expect:

```
In [7]: s = Series(['aaa', 'b', 'c'])

In [8]: s.isin('aaa')
Out[8]:
0    True
1    False
2    False
dtype: bool
```

or a raise....but raising seems inconsistent with the fact that single strings work....

this is happening bc the iterable passed in is turned into a set which is why a single string works



Discussion:
simple fix is to just turn single strings in to a list

other option is to deprecate this "feature" (it isn't tested) and enforce a list-like iterable to be passed. i'm sort of on the fence about it....practically speaking single strings are useful...but from a consistency standpoint it's ... well... inconsistent with the many of pandas other funcs/methods that take iterables

somewhat related is that **not** allowing single strings adds a bit more complexity to the `eval` parser since i must deal with things like `df['a == "b"']`. the `==` op must eval in python space. ideally that is just a  call to `a.isin('b')` but since that doesn't work for a general string it fails a bunch of my tests


Resolving patch:
diff --git a/pandas/core/frame.py b/pandas/core/frame.py
--- a/pandas/core/frame.py
+++ b/pandas/core/frame.py
@@ -4609,6 +4609,11 @@ def isin(self, values, iloc=False):
 
 
         else:
+            if not com.is_list_like(values):
+                raise TypeError("only list-like or dict-like objects are"
+                                " allowed to be passed to DataFrame.isin(), "
+                                "you passed a "
+                                "{0!r}".format(type(values).__name__))
             return DataFrame(lib.ismember(self.values.ravel(),
                                           set(values)).reshape(self.shape),
                              self.index,
diff --git a/pandas/core/series.py b/pandas/core/series.py
--- a/pandas/core/series.py
+++ b/pandas/core/series.py
@@ -2805,17 +2805,47 @@ def take(self, indices, axis=0, convert=True):
 
     def isin(self, values):
         """
-        Return boolean vector showing whether each element in the Series is
-        exactly contained in the passed sequence of values
+        Return a boolean :ref:`~pandas.Series` showing whether each element in
+        the ref:`~pandas.Series` is exactly contained in the passed sequence of
+        ``values``.
 
         Parameters
         ----------
-        values : sequence
+        values : list-like
+            The sequence of values to test. Passing in a single string will
+            raise a ``TypeError``:
+
+                .. code-block:: python
+
+                   from pandas import Series
+                   s = Series(list('abc'))
+                   s.isin('a')
+
+            Instead, turn a single string into a ``list`` of one element:
+
+                .. code-block:: python
+
+                   from pandas import Series
+                   s = Series(list('abc'))
+                   s.isin(['a'])
 
         Returns
         -------
-        isin : Series (boolean dtype)
+        isin : Series (bool dtype)
+
+        Raises
+        ------
+        TypeError
+          * If ``values`` is a string
+
+        See Also
+        --------
+        pandas.DataFrame.isin
         """
+        if not com.is_list_like(values):
+            raise TypeError("only list-like objects are allowed to be passed"
+                            " to Series.isin(), you passed a "
+                            "{0!r}".format(type(values).__name__))
         value_set = set(values)
         result = lib.ismember(_values_from_object(self), value_set)
         return self._constructor(result, self.index, name=self.name)


Test patch:
diff --git a/pandas/tests/test_frame.py b/pandas/tests/test_frame.py
--- a/pandas/tests/test_frame.py
+++ b/pandas/tests/test_frame.py
@@ -10915,6 +10915,16 @@ def test_isin_dict(self):
         expected.iloc[0, 0] = True
         assert_frame_equal(result, expected)
 
+    def test_isin_with_string_scalar(self):
+        #GH4763
+        df = DataFrame({'vals': [1, 2, 3, 4], 'ids': ['a', 'b', 'f', 'n'],
+                        'ids2': ['a', 'n', 'c', 'n']},
+                        index=['foo', 'bar', 'baz', 'qux'])
+        with tm.assertRaises(TypeError):
+            df.isin('a')
+
+        with tm.assertRaises(TypeError):
+            df.isin('aaa')
 
 if __name__ == '__main__':
     # unittest.main()
diff --git a/pandas/tests/test_series.py b/pandas/tests/test_series.py
--- a/pandas/tests/test_series.py
+++ b/pandas/tests/test_series.py
@@ -4433,6 +4433,16 @@ def test_isin(self):
         expected = Series([True, False, True, False, False, False, True, True])
         assert_series_equal(result, expected)
 
+    def test_isin_with_string_scalar(self):
+        #GH4763
+        s = Series(['A', 'B', 'C', 'a', 'B', 'B', 'A', 'C'])
+        with tm.assertRaises(TypeError):
+            s.isin('a')
+
+        with tm.assertRaises(TypeError):
+            s = Series(['aaa', 'b', 'c'])
+            s.isin('aaa')
+
     def test_fillna_int(self):
         s = Series(np.random.randint(-100, 100, 50))
         s.fillna(method='ffill', inplace=True)