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 pageshowhide
explore
- Decorator Mechanics34 questions
- Plain Decorators and @ Syntax3 questions
- Decorator Factories3 questions
- Stacking and Application Order3 questions
- functools.wraps and Metadata3 questions
- Stateful and Class-Based Decorators3 questions
- Decorating Classes4 questions
- Wrapping Methods and Descriptors4 questions
- Wrapping Coroutine Functions3 questions
- lru_cache and cached_property4 questions
- singledispatch Generic Functions4 questions
- The Iterator Protocol10 questions
- __iter__, __next__ and StopIteration3 questions
- Iterables vs Iterators3 questions
- Writing Custom Iterables4 questions
- Generator Functions23 questions
- yield and Suspended State4 questions
- send, throw and close4 questions
- Generators as Coroutines-Lite4 questions
- Laziness, Memory and Streaming4 questions
- yield from and Delegation4 questions
- One-Shot Iteration Traps3 questions
- itertools Patterns15 questions
- Infinite Iterators and Slicing4 questions
- Chaining and Accumulating4 questions
- Combinatorial Generators4 questions
- groupby and Sorted Input3 questions
- Iteration Builtins11 questions
- next() with Default Values3 questions
- any(), all() and Short-Circuiting4 questions
- enumerate, zip and reversed4 questions
questions
93 · 5 sectionsWhat does the @ decorator syntax above a Python def actually do?
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.
What do the maxsize and typed arguments to functools.lru_cache control?
basics
~20 smaxsize 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.
What object does a Python class decorator receive, and what does the class name end up bound to?
basics
~20 sA 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.
Why must a decorator wrapping an `async def` function define its wrapper with `async def` and await inside?
basics
~20 sCalling 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.
Why does @retry(times=3) need one more function layer than a bare @retry?
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.
How do you make a custom Python class iterable in a `for` loop?
basics
~20 sGive 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.
What is the difference between an iterable and an iterator in Python?
basics
~20 sAn 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.
What does a Python for loop do under the hood with iter() and next()?
basics
~20 sA 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.
When should a Python class's `__iter__` return `self` instead of a fresh iterator?
basics
~20 sReturn 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.
Why can a list be looped over repeatedly while a generator object is exhausted after one pass?
basics
~20 sEach 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.
Why does a second for loop over the same generator object produce nothing?
basics
~20 sA 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.
How does a generator expression's memory use differ from a list comprehension?
basics
~20 sA 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.
What does `yield from` do inside a Python generator function?
basics
~20 syield 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.
What does calling a Python generator function return, and when does its body first run?
basics
~20 sCalling 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.
Why can a Python 3 map, filter, zip or enumerate object be consumed only once?
basics
~20 sIn 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.
What is the difference between itertools.permutations and itertools.combinations?
basics
~20 sitertools.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)!).
Why does itertools.groupby split one key into several groups on unsorted input?
basics
~20 sitertools.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.
How do you bound itertools.count(), cycle() and repeat() so a loop terminates?
basics
~10 sitertools.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.
How do itertools.chain and itertools.chain.from_iterable differ when flattening a list of lists?
basics
~20 sBoth 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.
How does itertools.zip_longest differ from the built-in zip on unequal-length inputs?
basics
~10 sThe 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.
What do Python's any() and all() return for an empty iterable?
basics
~10 sany() 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.
What does enumerate(items, start=1) give you that range(len(items)) does not?
basics
~20 senumerate 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.
What does next(it, default) return when the iterator is already exhausted?
basics
~10 sIt 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.
Why does any() lose its short-circuit benefit when passed a list comprehension?
basics
~20 sArguments 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.
Why can zip() silently drop data, and how does zip(strict=True) prevent it?
basics
~20 szip 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.