Containers are what they sound like: they’re objects that hold values. Importantly, they support membership tests: they return boolean values for whether a value exists or not within the container. Lists, sets, dictionaries, tuples, and strings are all containers. For us, it’s important to have a base understanding of this because containers are also iterables.
The most important definition of an iterable is that it can return an iterator. In turn, an iterator is a specific type of Python class. What makes it unique is that saves the current state of the object (it doesn’t reset itself). What makes them so popular is that they save memory. In a sense, the iterator is a “lazy value factory”. When an iterator is created from an iterable, it will produce each value of the iterable one by one. This can allow for infinite sequences, infinite sequences from finite sequences, and finite sequences from infinite sequences.
Generators are a special kind of iterator. (Every generator is an iterator but not every iterator is a generator.) There are two types: generator functions and generator expressions. The function is any function which the return keyword has been replaced by yield. This function will return a generator object. The generator expression is the equivalent of a list comprehension.