6
class test(object):
    def __init__(self):
        pass
    def __iter__(self):
        return "my string"

o = test()
print iter(o)

Why does this give a traceback ?

$ python iter_implement.py
Traceback (most recent call last):
  File "iter_implement.py", line 9, in <module>
    print iter(o)
TypeError: iter() returned non-iterator of type 'str'

I was hoping __iter__ to just return the string in this case. When and why is it detected that the returned object is not an iterator object ?

Ankur Agarwal
  • 23,692
  • 41
  • 137
  • 208

4 Answers4

9

The code can be repaired by adding an iter() call:

class test(object):
    def __init__(self):
        pass
    def __iter__(self):
        return iter("my string")

Here is a sample run:

>>> o = test()
>>> iter(o)
<iterator object at 0x106bfa490>
>>> list(o)
['m', 'y', ' ', 's', 't', 'r', 'i', 'n', 'g']

The reason for the original error is that the API for __iter__ purports to return an actual iterator. The iter() function checks to make sure the contract is fulfilled.

Note, this kind of error checking occurs in other places as well. For example, the len() function checks to make sure a __len__() method returns an integer:

>>> class A:
        def __len__(self):
            return 'hello'

>>> len(A())
Traceback (most recent call last):
  File "<pyshell#4>", line 1, in <module>
    len(A())
TypeError: 'str' object cannot be interpreted as an integer
Raymond Hettinger
  • 216,523
  • 63
  • 388
  • 485
  • _The iter() function checks to make sure the contract is fulfilled._ ... not very thoroughly, though ;-) Well, probably good enough to catch the odd foot shot. – Paul Panzer Jan 06 '18 at 06:21
8

str is an iterable but not an iterator, subtle but important difference. See this answer for an explanation.

You want to return an object with __next__ (or just next if py2) which is what str returns when is iterated.

def __iter__(self):
      return iter("my string")

str does not implement __next__

In [139]: s = 'mystring'

In [140]: next(s)
---------------------------------------------------------------------------
TypeError                                 Traceback (most recent call last)
<ipython-input-140-bc0566bea448> in <module>()
----> 1 next(s)

TypeError: 'str' object is not an iterator

However calling iter returns the iterator, which is what looping calls:

In [141]: next(iter(s))
Out[141]: 'm'

You get the same problem returning anything without a __next__ (or next in py2) method

You can use a generator, which itself has __iter__ that returns self:

def gen():
    yield 'foo'

gg = gen()

gg is gg.__iter__()
True

gg.__next__()
'foo'

class Something:
     def __iter__(self):
         return gen()

list(Something())
['foo']

Or a class where you implement __next__ yourself, like this class similar to the one on the Ops post (you also have to handle StopIteration which stops the loop)

class test:
    def __init__(self, somestring):
        self.s = iter(somestring)

    def __iter__(self):
        return self

    def __next__(self):
        return next(self.s) ## this exhausts the generator and raises StopIteration when done.

In [3]: s = test('foo')

In [4]: for i in s:
   ...:     print(i)
   ...:
f
o
o
salparadise
  • 5,699
  • 1
  • 26
  • 32
  • Question is not how to fix the code. The question is why the error? I know the difference between iterable and iterator. – Ankur Agarwal Jan 06 '18 at 05:14
  • 1
    @abc apologies that you knew the difference, but pointing it out will surely benefit other people. Also, added an explanation. – salparadise Jan 06 '18 at 05:29
2

The purpose of __iter__ magic function is to return something that you can iterate (e.g. loop) through. The most common solution is to return iter(something) where something could be a list, a tuple, set, dictionary, a string... anything that we can iterate through. Take a look at this example:

class Band:
    def __init__(self):
        self.members = []

    def add_member(self, name):
        self.members.append(name)

    def __iter__(self):
        return iter(self.members)

if __name__ == '__main__':
    band = Band()
    band.add_member('Peter')
    band.add_member('Paul')
    band.add_member('Mary')

    # Magic of __iter__:
    for member in band:
        print(member)

Output:

Peter
Paul
Mary

In this case, the __iter__ magic function allows us to loop through band as if it is a collection of members. That means in your case, return "my string" will not do. If you want a list of chars in "my string":

def __iter__(self):
    return iter("my string")  # ==> m, y, ' ', s, t, r, i, n, g

However, if you want to return a list with a single element "my string", then:

def __iter__(self):
    return iter(["my string"])
Hai Vu
  • 37,849
  • 11
  • 66
  • 93
2

To answer your specific question. Python2 appears to check for the presence of a .next class attribute:

>>> class test(object):
...     next = None
...     def __iter__(self):
...         return self
... 
>>> print iter(test())
<__main__.test object at 0x7fcef75c2f50>

An instance attribute won't do:

>>> class test(object):
...    def __init__(self):
...        self.next = None
...    def __iter__(self):
...        return self
... 
>>> print iter(test())
Traceback (most recent call last):
  File "<stdin>", line 1, in <module>
TypeError: iter() returned non-iterator of type 'test'
Paul Panzer
  • 51,835
  • 3
  • 54
  • 99