and as usualwe can look under the hood to see how this actually works in iteration contexts running our class instance through iter obtains the result of calling __iter__ as usualbut in this case the result is generator object with an automatically created __next__ of the same sort we always get when calling generator function that contains yield the only difference here is that the generator function is automatically called on iter invoking the result object' next interface produces results on demands squares( runs __init__class saves instance state iter(sruns __iter__returns generator next( next(iruns generator' __next__ etc next(igenerator has both instance and local scope state stopiteration it may also help to notice that we could name the generator method something other than __iter__ and call manually to iterate--squares( , gen()for example using the __iter__ name invoked automatically by iteration tools simply skips manual attribute fetch and call stepclass squaresnon __iter__ equivalent (squares_manual pydef __init__)def gen(self)for value in range(self startself stop )yield value * python from squares_manual import squares for in squares( gen()print(iend='same results squares( iter( gen()next(isame results call generator manually for iterable/iterator coding the generator as __iter__ instead cuts out the middleman in your codethough both schemes ultimately wind up creating new generator object for each iterationwith __iter__iteration triggers __iter__which returns new generator with __next__ iterable objects__iter__ and __next__