Repository: pandas-dev/pandas

Issue:
series str as an iterator
This was [noticed on SO](http://stackoverflow.com/questions/16617394/pandas-how-to-select-by-partial-label-in-index/16617491?noredirect=1#comment23892039_16617491) by @DSM. It seems that the Series `str` method is a never ending iterator. I have no idea whether this should be classified as a bug, or just misuse. But here goes:

```
In [224]: g = (i for i in ds.str)

In [225]: next(g)
Out[225]:
google       NaN
wikimedia    NaN
wikipedia    NaN
wikitravel   NaN
dtype: float64

In [226]: next(g)
Out[226]:
google       NaN
wikimedia    NaN
wikipedia    NaN
wikitravel   NaN
dtype: float64

In [227]: next(g)
Out[227]:
google       NaN
wikimedia    NaN
wikipedia    NaN
wikitravel   NaN
dtype: float64

In [228]: next(g)
Out[228]:
google       NaN
wikimedia    NaN
wikipedia    NaN
wikitravel   NaN
dtype: float64

In [229]: list(g)  # lalalala
```



Discussion:
Not sure if this is a bug or a feature:

the reason `__getitem__` exists in `str` (and thus its invoked when used in list context)

is that

```
In [18]: ds
Out[18]: 
google        40
wikimedia     22
wikipedia     10
wikitravel    33
dtype: int64

In [19]: Series(ds.index).str[0:5]
Out[19]: 
0    googl
1    wikim
2    wikip
3    wikit
dtype: object
```

not sure how useful _that_ is though

u could have it iterate like `zip` and just throw `StopIteration` when the last character of the shortest string is reached **or** u could iterate to the last char of the longest and provide nans for the rest. the latter seems more pandas like

I'm a bit confused, the `next(g)` above is always the same (length of indexes doesn't seem to change).


Resolving patch:
diff --git a/pandas/core/strings.py b/pandas/core/strings.py
--- a/pandas/core/strings.py
+++ b/pandas/core/strings.py
@@ -661,6 +661,14 @@ def __getitem__(self, key):
         else:
             return self.get(key)
 
+    def __iter__(self):
+        i = 0
+        g = self.get(i)
+        while g.notnull().any():
+            yield g
+            i += 1
+            g = self.get(i)
+
     def _wrap_result(self, result):
         return Series(result, index=self.series.index,
                       name=self.series.name)


Test patch:
diff --git a/pandas/tests/test_strings.py b/pandas/tests/test_strings.py
--- a/pandas/tests/test_strings.py
+++ b/pandas/tests/test_strings.py
@@ -10,6 +10,8 @@
 
 from numpy import nan as NA
 import numpy as np
+from numpy.testing import assert_array_equal
+from numpy.random import randint
 
 from pandas import (Index, Series, TimeSeries, DataFrame, isnull, notnull,
                     bdate_range, date_range)
@@ -25,6 +27,82 @@ class TestStringMethods(unittest.TestCase):
 
     _multiprocess_can_split_ = True
 
+    def test_iter(self):
+        # GH3638
+        strs = 'google', 'wikimedia', 'wikipedia', 'wikitravel'
+        ds = Series(strs)
+
+        for s in ds.str:
+            # iter must yield a Series
+            self.assert_(isinstance(s, Series))
+
+            # indices of each yielded Series should be equal to the index of
+            # the original Series
+            assert_array_equal(s.index, ds.index)
+
+            for el in s:
+                # each element of the series is either a basestring or nan
+                self.assert_(isinstance(el, basestring) or isnull(el))
+
+        # desired behavior is to iterate until everything would be nan on the
+        # next iter so make sure the last element of the iterator was 'l' in
+        # this case since 'wikitravel' is the longest string
+        self.assertEqual(s.dropna().values.item(), 'l')
+
+    def test_iter_empty(self):
+        ds = Series([], dtype=object)
+
+        i, s = 100, 1
+
+        for i, s in enumerate(ds.str):
+            pass
+
+        # nothing to iterate over so nothing defined values should remain
+        # unchanged
+        self.assertEqual(i, 100)
+        self.assertEqual(s, 1)
+
+    def test_iter_single_element(self):
+        ds = Series(['a'])
+
+        for i, s in enumerate(ds.str):
+            pass
+
+        self.assertFalse(i)
+        assert_series_equal(ds, s)
+
+    def test_iter_numeric_try_string(self):
+        # behavior identical to empty series
+        dsi = Series(range(4))
+
+        i, s = 100, 'h'
+
+        for i, s in enumerate(dsi.str):
+            pass
+
+        self.assertEqual(i, 100)
+        self.assertEqual(s, 'h')
+
+        dsf = Series(np.arange(4.))
+
+        for i, s in enumerate(dsf.str):
+            pass
+
+        self.assertEqual(i, 100)
+        self.assertEqual(s, 'h')
+
+    def test_iter_object_try_string(self):
+        ds = Series([slice(None, randint(10), randint(10, 20))
+                     for _ in xrange(4)])
+
+        i, s = 100, 'h'
+
+        for i, s in enumerate(ds.str):
+            pass
+
+        self.assertEqual(i, 100)
+        self.assertEqual(s, 'h')
+
     def test_cat(self):
         one = ['a', 'a', 'b', 'b', 'c', NA]
         two = ['a', NA, 'b', 'd', 'foo', NA]