skip to content

Decorators and Generators

Decorators wrap a callable to add behavior without editing its body; generators suspend a function to yield values lazily. Interviewers pair them: both rest on closures and the iterator protocol.

part ofPythonoverview, primer and where to startread it →
on this pageshow

explore

questions

93 · 5 sections

What does the @ decorator syntax above a Python def actually do?

level: juniorimportance: must knowfreq 85%
basics
~20 s

@deco written above def f() is shorthand for defining f and then rebinding that name: f = deco(f). A decorator is any callable that takes the function object and returns the object the name will point at.

open as a page

What do the maxsize and typed arguments to functools.lru_cache control?

level: juniorimportance: must knowfreq 50%
basics
~20 s

maxsize caps how many results are stored - 128 by default, None for unbounded, 0 for none - and a full table drops the least recently used entry. typed=True adds argument types to the key, so 1 and 1.0 stop sharing.

open as a page

What object does a Python class decorator receive, and what does the class name end up bound to?

level: juniorimportance: must knowfreq 45%
basics
~20 s

A class decorator is called with the finished class object, after the class body has already executed, and whatever it returns is bound to the class name. Most decorators mutate the class and return the same object.

open as a page

Why must a decorator wrapping an `async def` function define its wrapper with `async def` and await inside?

level: juniorimportance: must knowfreq 60%
basics
~20 s

Calling an async def function runs none of its body; it returns a coroutine object. A plain def wrapper only holds that unstarted coroutine, so anything it does around the call observes nothing. The wrapper must be async def and await inside.

open as a page

Why does @retry(times=3) need one more function layer than a bare @retry?

level: juniorimportance: must knowfreq 60%
basics
~20 s

@retry(times=3) calls retry(times=3) first and decorates with whatever that call returns. So retry must be a factory that returns a decorator, which then takes the function and returns the wrapper - three nested levels instead of two.

open as a page

How do you make a custom Python class iterable in a `for` loop?

level: juniorimportance: must knowfreq 55%
basics
~20 s

Give the class an iter method. The simplest implementation is to write iter as a generator function that yields each element: Python then gets a fresh iterator on every loop, and you never write next or raise StopIteration yourself.

open as a page

What is the difference between an iterable and an iterator in Python?

level: juniorimportance: must knowfreq 85%
basics
~20 s

An iterable can hand out a fresh iterator via iter. An iterator is the cursor itself: it defines next to produce one item at a time, and its own iter returns self, so every iterator is also an iterable.

open as a page

What does a Python for loop do under the hood with iter() and next()?

level: juniorimportance: must knowfreq 70%
basics
~20 s

A for loop calls iter() on the object once to get an iterator, then calls next() on that iterator repeatedly, binding each result to the loop variable. When next() raises StopIteration, the loop catches it and ends normally.

open as a page

When should a Python class's `__iter__` return `self` instead of a fresh iterator?

level: middleimportance: must knowfreq 48%
basics
~20 s

Return self only when the object is itself a cursor over a source that cannot be replayed, such as a stream reader; it is then one-shot. A container that must survive repeated and nested loops builds a new iterator per call.

open as a page

Why can a list be looped over repeatedly while a generator object is exhausted after one pass?

level: middleimportance: should knowfreq 70%
basics
~20 s

Each loop over a list calls iter() and gets a brand-new cursor starting at the first element. A generator object is already the cursor: its iter returns self, so a second loop resumes at the end and finishes without running the body.

open as a page

Why does a second for loop over the same generator object produce nothing?

level: juniorimportance: must knowfreq 72%
basics
~20 s

A generator object is its own iterator and holds one position. Once it has raised StopIteration it is exhausted for good, so a second for loop starts at the end and finishes immediately, silently and without an error.

open as a page

How does a generator expression's memory use differ from a list comprehension?

level: juniorimportance: must knowfreq 78%
basics
~20 s

A list comprehension builds every element and holds them all at once. A generator expression builds an object that produces one element per next() call, so peak memory tracks a single item rather than the whole result.

open as a page

What does `yield from` do inside a Python generator function?

level: juniorimportance: must knowfreq 70%
basics
~20 s

yield from <iterable> yields every value that iterable produces, replacing an explicit for-loop that re-yields inside a generator function. Unlike that loop, it also forwards send, throw and close to a subgenerator and binds its return value.

open as a page

What does calling a Python generator function return, and when does its body first run?

level: juniorimportance: must knowfreq 78%
basics
~20 s

Calling it runs none of the body. You get back a generator object, which is an iterator. The body starts only on the first next() call and runs up to the first yield, then pauses there.

open as a page

Why can a Python 3 map, filter, zip or enumerate object be consumed only once?

level: middleimportance: must knowfreq 58%
basics
~20 s

In Python 3 these builtins return lazy iterator objects rather than lists. Each is its own iterator with a single forward cursor, so the first consumer drains it and every later consumer sees an empty sequence.

open as a page

What is the difference between itertools.permutations and itertools.combinations?

level: juniorimportance: must knowfreq 62%
basics
~20 s

itertools.permutations treats order as significant, so ('a','b') and ('b','a') are both yielded. itertools.combinations yields each selection once, keeping the input's own order. For n items taken r at a time that is n!/(n-r)! tuples versus n!/(r!(n-r)!).

open as a page

Why does itertools.groupby split one key into several groups on unsorted input?

level: juniorimportance: must knowfreq 38%
basics
~20 s

itertools.groupby groups only consecutive items whose computed key is equal, and it never sorts. On unsorted input the same key reappears later and starts a fresh group, so sort by the same key function first.

open as a page

How do you bound itertools.count(), cycle() and repeat() so a loop terminates?

level: juniorimportance: must knowfreq 60%
basics
~10 s

itertools.count, cycle and repeat never raise StopIteration, so draining one runs forever. The consumer has to stop them: wrap with itertools.islice or itertools.takewhile, zip against a finite iterable, or break out of the loop.

open as a page

How do itertools.chain and itertools.chain.from_iterable differ when flattening a list of lists?

level: middleimportance: must knowfreq 55%
basics
~20 s

Both yield the items of the inner iterables one level flatter, lazily. itertools.chain(*rows) unpacks the outer sequence into arguments first, so it must be finite and in memory; chain.from_iterable(rows) takes the outer iterable itself and pulls it lazily.

open as a page

How does itertools.zip_longest differ from the built-in zip on unequal-length inputs?

level: juniorimportance: should knowfreq 48%
basics
~10 s

The built-in zip stops at the shortest input and silently discards the rest. itertools.zip_longest runs until the longest input is exhausted and substitutes fillvalue, which defaults to None, for every missing element.

open as a page

What do Python's any() and all() return for an empty iterable?

level: juniorimportance: must knowfreq 62%
basics
~10 s

any() returns False and all() returns True on an empty iterable. all() is vacuously true because no element failed the test; any() found no truthy element, so there is nothing to report as True.

open as a page

What does enumerate(items, start=1) give you that range(len(items)) does not?

level: juniorimportance: must knowfreq 75%
basics
~20 s

enumerate yields (index, item) pairs straight from any iterable, so the loop body never indexes back into the sequence and the object needs no len(). The start argument only shifts the counter; it never skips items.

open as a page

What does next(it, default) return when the iterator is already exhausted?

level: juniorimportance: must knowfreq 45%
basics
~10 s

It returns the second argument. With only one argument, next raises StopIteration on an exhausted iterator; supplying a fallback turns exhaustion into an ordinary return value. Any other exception the iterator raises still propagates.

open as a page

Why does any() lose its short-circuit benefit when passed a list comprehension?

level: middleimportance: must knowfreq 55%
basics
~20 s

Arguments are evaluated before the call. A list comprehension runs the predicate on every element and allocates the whole list first, so any() short-circuits over a result that has already cost full time and memory.

open as a page

Why can zip() silently drop data, and how does zip(strict=True) prevent it?

level: middleimportance: must knowfreq 60%
basics
~20 s

zip stops the moment its shortest input is exhausted, so trailing items of longer inputs are dropped with no warning. Since Python 3.10, passing strict=True makes zip raise ValueError when the inputs turn out to be unequal in length.

open as a page