The iterator protocol
An iterable is anything that knows how to produce an iterator from itself, by implementing __iter__. An iterator is anything that produces values one at a time, by implementing __next__. When the iterator runs out, __next__ raises StopIteration.
A for loop is built on these two methods. It calls __iter__ on the iterable to get an iterator, then calls __next__ repeatedly, binding each value to the loop variable. When StopIteration shows up, the loop exits cleanly.
This is why a list, a string, a dict, a file, and a custom class can all be used in the same for loop. They all implement the same two methods. The protocol is the entire mechanism behind iteration in Python.