Every for loop you have written relies on one small protocol: ask an object for an iterator, then ask the iterator for the next item until it says it has none left. Once you know that protocol, you can write your own sequences that produce values lazily, one at a time, as the loop asks for them.
Lazy sequences let you process a file larger than your memory, describe an endless stream of IDs, or chain several processing steps without building a list between them. Generators make them short to write, and the itertools module supplies ready made building blocks.
You will be able to:
- Tell an iterable from an iterator and drive one by hand with
iter()andnext() - Explain what a
forloop does behind the scenes - Write a small iterator class and, more often, a generator function
- Use generator expressions,
yield fromand infinite generators, and reason about laziness and memory - Choose the right
itertoolsfunction for counting, slicing, chaining, grouping, windows and combinations - Build a lazy pipeline that processes data one item at a time
Iterables and iterators
An iterable is any object you can loop over: lists, tuples, strings, dicts, sets, ranges and open files are all iterables. Calling iter() on an iterable gives you an iterator, an object that hands out the items one at a time. Calling next() on the iterator returns the next item, and when there are none left it raises StopIteration. next(iterator, default) returns default instead of raising.
| Iterable | Iterator | |
|---|---|---|
| What it can do | give you an iterator with iter(obj) | give you the next item with next(it) |
| Special methods | __iter__ (or a sequence's __getitem__) | __iter__ (returning itself) and __next__ |
| Can you loop over it twice? | yes, each loop gets a fresh iterator | no, it is used up after one pass |
| Examples | list, str, dict, range | the result of iter([...]), map, filter, zip, enumerate, generators, open files |
Because an iterator's __iter__ returns the iterator itself, you can loop over an iterator too, but only once.
Pitfall.
map,filter,zipand generators are iterators, so they are consumed by the first thing that reads them. A second pass finds nothing:
squares = map(lambda n: n * n, [1, 2, 3])→list(squares)gives[1, 4, 9], and a secondlist(squares)gives[].If you need the values twice, store them in a list first.
A range is different: it is a lazy iterable, not an iterator, so iter(range(3)) returns a new iterator each time and you can loop over the same range as often as you like.
What a for loop does
A for loop calls iter() once, then calls next() before each pass of the body. When next() raises StopIteration, the loop catches it, runs the else block if there is one, and ends. You never see the exception.
This while loop does the same work as for item in data: print(item):
Writing an iterator class
To make your own iterator, write a class with two special methods: __iter__, which returns the iterator itself, and __next__, which returns the next value or raises StopIteration. Classes are covered properly in Classes and objects; here you need only the shape.
That is a lot of ceremony for "count down", and the object has to remember by hand, in self.current, where it is between calls. A generator does the same job in a few lines and remembers its place for you.
Generators
A generator function is a function that contains yield. Calling it doesn't run the body; it returns a generator, which is an iterator. Each next() runs the body until the next yield, hands out that value and pauses, keeping all local variables. When the body finishes, the generator raises StopIteration for you.
Notice that starting appears only at the first next(), not when countdown(3) is called, and that the for loop carries on from where the manual next() calls stopped.
Generator expressions
A generator expression looks like a list comprehension in parentheses: (n * n for n in numbers). It builds a generator instead of a list. When it is the only argument of a call, you can drop the extra parentheses: sum(n * n for n in numbers).
Laziness and memory
A list holds all its items at once. A generator holds only its current state, so its size doesn't depend on how many items it will produce.
On 64 bit CPython 3.15 this printed about 8.4 million bytes for the list and about 200 for the generator; your output will differ. sys.getsizeof counts only the list's own array of references, not the int objects in it, so the real gap is even larger.
Laziness also means work happens only when someone asks for a value. any() stops at the first true item, so the generator below never examines 5 or 7:
The price of laziness: a generator has no len(), no indexing, and only one pass. Errors in the generating code appear when the values are consumed, not when the generator is created.
yield from
yield from iterable yields every item of another iterable in turn. It replaces a for loop whose body is only yield item, and it is handy for recursion:
Infinite generators
A generator doesn't have to end. An endless stream is fine as long as the consumer takes only what it needs, for example with itertools.islice, zip or break:
Pitfall.
list(),sum(),sorted()or a plainforloop over an infinite generator never finishes. Always bound it first.
The itertools toolbox
The itertools module provides fast, lazy building blocks. All of them return iterators.
| Function | Yields | Example | Result |
|---|---|---|---|
count(start, step) | start, start + step, ... forever | count(10, 5) | 10, 15, 20, ... |
cycle(it) | the items of it, repeated forever | cycle("ab") | a, b, a, b, ... |
repeat(x, n) | x, n times (forever without n) | repeat("x", 3) | x, x, x |
islice(it, start, stop, step) | a slice of any iterable | islice("abcdef", 1, 5, 2) | b, d |
chain(a, b, ...) | the items of a, then of b, ... | chain([1, 2], "ab") | 1, 2, a, b |
accumulate(it, func) | running totals (or running func) | accumulate([1, 2, 3, 4]) | 1, 3, 6, 10 |
groupby(it, key) | (key, group) for each run of equal keys | groupby("aabccc") | runs of a, b, c |
pairwise(it) (3.10+) | overlapping pairs | pairwise("abcd") | ab, bc, cd |
batched(it, n) (3.12+) | tuples of up to n items | batched("abcdefg", 3) | abc, def, g |
product(a, b) | every combination of one item from each | product("ab", "xy") | ax, ay, bx, by |
permutations(it, r) | ordered arrangements of r items | permutations("abc", 2) | 6 tuples |
combinations(it, r) | unordered selections of r items | combinations("abcd", 2) | 6 tuples |
Counting, cycling, slicing and chaining
zip stops at its shortest input, which is what makes it safe to pair a finite list with an infinite cycle.
Running totals and grouping
accumulate yields running results. groupby yields a key and an iterator over each run of consecutive items with the same key, so you normally sort by the same key first.
Pitfall. Each group shares the underlying iterator with
groupby. Whengroupbymoves to the next key, the previous group is gone, so turn a group into a list straight away if you need to keep it.
Windows and chunks
pairwise (since 3.10) yields overlapping pairs, ideal for differences between neighbours. batched (since 3.12) cuts an iterable into tuples of n items; the last one may be shorter, and since 3.13 strict=True raises ValueError instead.
Combinatorics
These grow fast: permutations of 10 items yields 3,628,800 tuples. If you need only the count, use math.perm and math.comb.
Generator pipelines
Small generators chain into a pipeline: each stage takes an iterable, does one job and yields results to the next. Nothing runs until the last stage pulls a value, and then each line flows through all the stages before the next line is taken. Memory use stays flat however large the file is. The first stage opens the file with with open(...), which closes it again when the block ends; Errors and exceptions explains with.
Each stage is easy to test on its own with a small list, and you can swap one out, for example reading from a network stream instead of a file, without touching the others.
send and close, briefly
Generators have two more methods you will meet occasionally:
gen.send(value)resumes the generator and makes the pausedyieldexpression evaluate tovalue. You must start the generator withnext(gen)(orsend(None)) first; sending anything else to a fresh generator raisesTypeError: can't send non-None value to a just-started generator.gen.close()raisesGeneratorExitat the pausedyield, so anyfinallyblock runs and the generator ends.
These methods were once the basis for coroutines. Modern asynchronous code uses async def and await instead (see Concurrency), so in everyday code you will mostly write plain generators.
Check yourself
- What is the difference between an iterable and an iterator?
- What does a
forloop do when its iterator raisesStopIteration? nums = (n for n in range(4)). Why doessum(nums)return6the first time and0the second?- What happens when you call a generator function?
- When would you choose a list comprehension over a generator expression?
- Why do you usually sort data before passing it to
groupby? - What is the difference between
pairwiseandbatched? - What does
close()do to a paused generator?
Suggested answers
- An iterable can produce an iterator through
iter(), and can usually be looped over many times. An iterator produces items throughnext(), remembers its position, and is used up after one pass. - It catches the exception, runs the loop's
elseblock if there is one, and ends the loop. - A generator expression is an iterator. The first
sumconsumes all the values; the second finds the iterator already exhausted. - Nothing in the body runs yet. You get a generator object; the body runs up to the first
yieldon the firstnext(). - When you need the values more than once, need
len()or indexing, or the data is small and a list is simpler to inspect. groupbygroups only consecutive items with equal keys. Sorting by the same key puts all equal items next to each other, giving one group per key.pairwiseyields overlapping pairs, each item paired with the next one.batchedyields nonoverlapping chunks ofnitems.- It raises
GeneratorExitat the pausedyield, sofinallyblocks run, and the generator finishes; furthernext()calls raiseStopIteration.
Exercises
These exercises are optional practice. Pick the ones that help you.
- Run length encoding. Using
itertools.groupby, writeencode(text)that writes each run of a repeated character as the character followed by the run length. Example:encode("aaabccdddd")gives"a3b1c2d4", andencode("")gives"". Runs are counted separately:encode("aabaa")gives"a2b1a2". - Fibonacci. Write an infinite generator
fibonacci()that yields 0, 1, 1, 2, 3, 5, 8, ... Then writefirst_with_digits(n)that returns the first Fibonacci number with at leastndigits. Example: the first ten values are[0, 1, 1, 2, 3, 5, 8, 13, 21, 34], andfirst_with_digits(3)gives144. - Chunked reader. Write a generator
read_chunks(path, size)that opens a UTF-8 text file and yields its content in strings of at mostsizecharacters, without reading the whole file at once. For a file containingabcdefghijandsize=4, it yields"abcd","efgh","ij". An empty file yields nothing. Asizebelow 1 raisesValueError. - Sliding window. Write
windows(iterable, k)that yields tuples ofkconsecutive items from any iterable, including a generator.windows([1, 2, 3, 4, 5], 3)yields(1, 2, 3),(2, 3, 4),(3, 4, 5). If there are fewer thankitems it yields nothing. Akbelow 1 must raiseValueErroras soon aswindowsis called, not at the firstnext().
Solutions
1. Run length encoding.
2. Fibonacci.
3. Chunked reader.
file.read(size) returns an empty string at the end of the file, which ends the while loop. The two argument form iter(callable, sentinel) does the same: iter(lambda: file.read(size), "") calls the function until it returns "". Here the size check runs at the first next(), which is fine because the test consumes the generator; exercise 4 shows how to check eagerly.
4. Sliding window.
A generator function's body, including any argument check, runs only at the first next(). Splitting the work into a plain function that validates and a generator that yields makes the error appear at the call. deque(maxlen=k) drops the oldest item automatically when a new one arrives (see Collections and comprehensions).