Free Handbook · Runs in your browser

Advanced Python

Comprehensions, the iterator protocol, generators traced yield by yield, closures, decorators built from scratch, and type hints that make editors and reviewers understand your code — the module that separates "can write Python" from "writes Python well".

0 / 108 lessons🔥 0 day streak
ShareXLinkedIn

Module 09 · what you'll be able to do

  • Write list, dict and set comprehensions and know when a plain loop is clearer
  • Explain iter() and next(), and why a file or a range does not load everything into memory
  • Write a generator with yield and trace exactly when each line runs
  • Build a decorator, understand closures underneath it, and use functools.wraps
  • Annotate functions and data with type hints and run a type checker
01

Comprehensions

A comprehension builds a list, dict or set from an iterable in one expression: [expr for item in iterable if condition]. It replaces the four-line "create empty, loop, test, append" pattern and is usually faster. The rule for readability: one for, at most one if, and if it does not fit on one line, write the loop.

pythonEdit it. ⌘/Ctrl + Enter runs.
You should see
True [9, 16, 25, 4]
{'apple': 5, 'fig': 3, 'banana': 6}
{3, 5, 6}
['even', 'odd', 'even', 'odd']
[1, 2, 3, 4, 5]
333332833333500000
Your turn
From orders = [{"id": 1, "total": 30}, {"id": 2, "total": 5}, {"id": 3, "total": 12}], make a dict of id → total for orders over 10, in one comprehension.

Too clever

  • [f(x) for x in xs if g(x) for y in h(x) if y]
  • A comprehension only for its side effect: [print(x) for x in xs]
  • Three levels of nesting

Just right

  • [x.strip() for x in lines if x]
  • A loop, when there is a side effect
  • Two comprehensions with a named intermediate
02

Iterators and the iterator protocol

A for loop is sugar for: call iter(thing) to get an iterator, then call next() on it until it raises StopIteration. Anything with __iter__ is iterable; anything with __next__ is an iterator. This is why files, ranges and zip objects can be huge without being in memory — they produce one item at a time.

pythonEdit it. ⌘/Ctrl + Enter runs.
You should see
10 20 30
exhausted — a for loop catches this for you
[('a', 1), ('b', 2)] []
[3, 2, 1]
10000000000 True
[5, 6, 7]
[1, 2, 3, 'a', 'b']
Quick check

You pass the same zip(a, b) object to two functions. The second one sees nothing. Why?

03

Generators

A function with yield in it is a generator: calling it runs nothing and returns an iterator. Each next() runs the body until the next yield, hands that value out, and pauses right there — locals intact. This is how you process a 50 GB file line by line, stream rows from a database, or produce an infinite sequence, in a few lines and constant memory.

VisualizeA generator pauses at yield and resumes on next()Step 1 / 4
def evens(limit):
n = 0
while n < limit:
yield n
n += 2
g = evens(5)
print(next(g))
print(next(g))
print(list(g))
Line 7

Calling evens(5) does NOT run the body. It creates a generator object and binds it to g.

Variables now
g<generator>
bodynot started
All 4 steps as a table
StepLineWhat happenedVariables now
17Calling evens(5) does NOT run the body. It creates a generator object and binds it to g.g = <generator> body = not started
28next(g) starts the body: n = 0, 0 < 5, reach yield n → hand out 0 and PAUSE on line 4.n = 0 body = paused at yield
39next(g) resumes after the yield: n += 2, loop check 2 < 5, yield 2, pause.n = 2
410list(g) keeps calling next: yields 4; then n = 6, 6 < 5 is False, the function ends → StopIteration → the list is done.n = 6 body = finished
pythonEdit it. ⌘/Ctrl + Enter runs.
You should see
ada 98
linus 87
grace 91
[98, 91]
[0, 1, 1, 2, 3, 5, 8, 13, 21, 34]
[1, 2, 3, 4, 5]
Your turn
Write batches(iterable, size) that yields lists of size items — then solve the practice problem below with it.
04

Closures

A function defined inside another function can use the outer function's variables — and keeps using them after the outer function has returned. That inner function plus the variables it captured is a closure. It is how you make a function "remember" something without a class, and it is the machinery decorators are built on.

pythonEdit it. ⌘/Ctrl + Enter runs.
You should see
20 30
2
1 2 3
[2, 2, 2]
[0, 1, 2]
05

Decorators

A decorator is a function that takes a function and returns a new one, usually wrapping the original with extra behaviour — timing, logging, caching, access checks, retries. The @name line above a def is just sugar for func = name(func). Once you see that, decorators stop being magic.

pythonEdit it. ⌘/Ctrl + Enter runs.
You should see
total was fast
499999500000
total | Sum 0..n-1.
attempt 1 failed: not yet
attempt 2 failed: not yet
ok on call 3
23416728348467685
Your turn
Write a @log_calls decorator that prints the function name and its arguments before each call. Apply it to two functions.
Always use functools.wraps
Without it the decorated function's __name__ becomes "wrapper", its docstring disappears, and every traceback and every tool that introspects it (frameworks, test runners, documentation) sees the wrapper instead.
Quick check

@timed above def total is equivalent to which line?

06

Type hints

Type hints annotate what a function expects and returns: def total(prices: list[float]) -> float. Python ignores them at runtime — they exist for editors (autocomplete, red squiggles), for readers, and for type checkers like mypy and pyright that find whole classes of bugs before the code runs. Every modern codebase uses them; they are expected in interviews.

pythonEdit it. ⌘/Ctrl + Enter runs.
You should see
2.1666666666666665
None
Alien area 9.0
mypy would catch mean(['a'])
{'values': typing.Iterable[float], 'return': <class 'float'>}
shellterminal — running a type checker
$ python -m pip install mypy
$ mypy app.py
app.py:12: error: Argument 1 to "mean" has incompatible type "list[str]"; expected "Iterable[float]"  [arg-type]
Found 1 error in 1 file (checked 1 source file)

VS Code's Pylance runs pyright continuously, so you see the same errors as you type.

HintMeansNote
int str float boolthe basic types
list[int] dict[str, float] set[str] tuple[int, str]containers with element typesLower-case built-ins since 3.9; List from typing is legacy
int | Nonean int or NoneOptional[int] is the older spelling
Iterable[T] Sequence[T]anything you can loop over / indexPrefer these for parameters — accept more, return less
Callable[[int, str], bool]a function taking (int, str) returning bool
Anyopt outUse sparingly; it turns the checker off for that value

Finish the Python handbook, then get hired

Sit the exam for your certificate, run your resume through the ATS checker, and see the jobs that ask for exactly this.

Check my resume
Found this course useful? Share it.
ShareXLinkedIn

Comments

0

Join the conversation. Sign in to leave a comment — we'd love to hear your thoughts.