Python distinguishes iterables — objects that can return an iterator via `__iter__` — from iterators, which implement both `__iter__` and `__next__`. When an iterator is exhausted, it raises `StopIteration`. Comprehensions provide concise syntax for building collections: list comprehensions such as `[x*2 for x in range(10)]` produce lists, while dict and set comprehensions use braces. Generator expressions like `(x*2 for x in range(10))` look similar but evaluate lazily, producing one value at a time without materializing the entire sequence.
Generator functions extend laziness to ordinary functions, using `yield` to return a value while suspending execution state. The `yield from` syntax delegates iteration to another iterable or generator. Built-in iteration tools complete the picture: `range` produces sequences of integers, `enumerate` yields `(index, value)` pairs, and `zip` combines multiple iterables element-wise — stopping at the shortest, unlike `itertools.zip_longest`, which pads with a fill value. `map` applies a function to each item, `filter` retains items where a function returns truthy, and `any` and `all` reduce an iterable to a boolean. `sum`, `min`, and `max` provide aggregate reductions. A list comprehension is generally more readable than the equivalent `list(map(...))` call.
For ordering, `sorted()` returns a new sorted list, while `list.sort()` sorts in place. Both accept a `key` function that extracts the comparison value from each element, and a `reverse=True` flag for descending order. When modifying a list, `append` adds a single item, while `extend` concatenates an iterable. Choosing between eager comprehensions and lazy generators is especially valuable when working with large or potentially infinite sequences.