Loading...
Loading...
Python interview guide
Comprehensions, generators, decorators, context managers and typing. Every answer is short enough to say aloud, and every question links to the lesson that teaches it.
45 questionsLast updated
An iterable has __iter__ and returns an iterator when iter() is called on it. An iterator has __next__ (and __iter__ returning itself) and produces one value per call. A list is iterable but not an iterator; iter(list) is an iterator. Iterators are single-pass, while iterables like lists can be iterated again.
It calls iter(obj) once to get an iterator, then calls next() on it in a loop. When the iterator raises StopIteration, the loop ends without an error. Any other exception propagates. This is why anything that follows the protocol, such as a file or a generator, works in a for loop.
__iter__ return self?So an iterator can be used wherever an iterable is expected. A for loop always calls iter() first; if that returned something new, it would restart. Returning self means the loop continues from the current position, which is why for x in it after a break carries on where it stopped.
An iterator keeps one position and never rewinds, so a second loop finds it used up. Fix it by making the class an iterable whose __iter__ returns a fresh iterator each time, by recreating the iterator, or by storing results in a list if they are needed again.
iter(callable, sentinel) do?It builds an iterator that calls the callable with no arguments on every step and stops when the returned value equals the sentinel. A classic use is iter(lambda: f.read(4096), b"") to read a file in chunks.
map, filter, zip, enumerate and reversed return lazy iterators, not lists. They use little memory and work on large data, but they can be consumed only once and do not support len() or indexing. Wrap them in list() when you need those.
Python raises RuntimeError: dictionary changed size during iteration, because the dictionary iterator cannot keep a reliable position. Loop over a copy such as list(d) or list(d.items()), or build a new dictionary instead.
StopIteration an error?No. It is the normal signal that the values ended; for loops and list() handle it. It becomes a problem only when you call next() yourself without a default or catch, or when it leaks out of a generator, where Python 3.7+ turns it into a RuntimeError.
A generator function contains yield. Calling it returns a generator object immediately without running the body. Each next() runs the body until the next yield, hands out the value and pauses, keeping local variables. A normal function runs to completion and returns once, losing its locals.
A list holds all values in memory at once and can be indexed and reused. A generator produces values one at a time on demand, uses constant memory, can be infinite, and can be consumed once. Use a list when you need random access or several passes, and a generator for a single pass over big or streaming data.
Lesson: Generator Functions and ExpressionsBoth have the same syntax apart from the brackets. [x for x in data] builds the full list immediately; (x for x in data) builds a lazy generator. Use the generator form inside sum, max, any or a for loop when you need one pass. Use a list comprehension when you need the result more than once.
next() calls?Python keeps the whole frame, including local variables and the position of the paused line. This is why a generator can remember counters or previous values without a class. The frame is freed when the generator finishes or is garbage collected.
Lesson: Generator Functions and Expressionssend() work, and what is a coroutine-style generator?g.send(value) resumes the generator and makes the paused yield expression evaluate to value, then runs to the next yield. The generator must be started with next(g) first. It lets a generator receive data as well as give it. Modern code uses async/await for coroutines, but send shows how they began.
return value do in a generator?It ends the generator. The value is stored in the value attribute of the StopIteration exception and is the result of a yield from expression. It is not one of the yielded values, so list(gen()) does not include it.
Reading line by line with a generator keeps only one line in memory, so a file larger than RAM can still be processed. The processing can also start before the whole file is read. Files themselves are lazy iterators, and generators let you chain filters on top of them.
Lesson: Generator Functions and ExpressionsYou cannot rewind it. Either call the generator function again to get a fresh generator, store the results in a list, or wrap the function in an iterable class whose __iter__ is a generator method, so each loop creates a new generator.
yield from do, and how is it different from a for loop with yield?yield from iterable yields every value from the iterable. For plain iterables the result looks the same as for x in iterable: yield x. The difference is with generators: yield from also forwards send(), throw() and close() to the inner generator, and its expression evaluates to the inner generator's return value.
Write a recursive generator: for each item, if it is a list (or other container you want to open), yield from flatten(item), otherwise yield item. It runs lazily and uses stack depth equal to the nesting depth. For very deep data, use an explicit stack to avoid the recursion limit. Remember strings are iterable, so check for them separately.
itertools.groupby give surprising results on unsorted data?It groups only consecutive items with the same key and does not look further. If equal keys are apart, they land in separate groups. Sort by the same key first, or use a dictionary or defaultdict when order does not matter. Each group is a lazy iterator that is invalidated when you move to the next group.
permutations(x, r) gives ordered arrangements without repeating items, so there are n!/(n-r)!. combinations(x, r) gives unordered selections, n!/(r!(n-r)!). product(a, b) gives the Cartesian product, every pair with one item from each input, with repeats allowed across inputs. All three are lazy, but the number of results grows fast.
Use itertools.islice(gen, N). It reads only N items, so it works on endless generators. After that the generator continues from item N+1. Plain slicing like gen[:N] fails because generators are not sequences.
chain(a, b) and chain.from_iterable(list_of_iterables)?chain(a, b) takes the iterables as separate arguments. chain.from_iterable(x) takes one iterable that yields iterables, and reads it lazily. Use it to flatten one level, for example chain.from_iterable(rows), especially when the rows come from a generator.
They are implemented in C, run fast, return lazy iterators with constant memory, and compose well: islice(chain(a, b), 10) never builds the joined list. They also express the intent in one name, which makes the code easier to read and review.
itertools.tee do, and what is its catch?tee(it, n) splits one iterator into n independent iterators. To make that possible it buffers the values that one copy has read and another has not. If one copy runs far ahead, the buffer grows large. After calling tee you must not use the original iterator.
A generator is shorter and usually enough. Write a class when the iterator must expose other attributes or methods (for example a peek() or a reset()), when you need a clear __repr__ or to pickle it, or when you are implementing an interface that expects an object. In most other cases, a generator function is easier to read and less error-prone.
Open the file and iterate over it line by line, since a file object is a lazy iterator. Chain generator stages for parsing and filtering, and aggregate with a running counter or sum. At no point is the whole file in memory. Use islice or an early break when only the first matches are needed.
A generator is a single-pass iterator and the first loop used it up. Fix it by returning a list when the data is small, by calling the generator function again for each pass, or by wrapping it in a class whose __iter__ creates a new generator each time.
It is a chain of generators in which each stage pulls one item at a time from the previous one. Advantages: low memory, early stopping, and clear stage-by-stage code. Risks: one pass only, errors appear late (when consumed), and debugging is harder because nothing runs until something iterates.
Lesson: Iterators and Generators Checkpointlist(groupby(data)) on unsorted data, and with using its groups after the list is built?Unsorted data gives split groups. Also each group is a lazy iterator that shares the underlying iterator, so after moving to the next group the earlier ones become empty. Consume each group (for example into a list or a sum) while looping.
Lesson: Iterators and Generators CheckpointA closure is a function object that remembers variables from the enclosing scope in which it was defined, even after that scope has finished. Python stores them in cells, which you can see in func.__closure__. Closures are the base of decorators, callbacks and function factories.
A closure looks up the value of a captured variable when it is called, not when it is created. Functions made in a loop therefore all see the final loop value. Fix it by binding the value through a default argument (lambda i=i: i), by using functools.partial, or by calling a factory function that creates a new scope for each value.
nonlocal do and how is it different from global?nonlocal name makes assignments to name change the variable in the nearest enclosing function scope. global name does the same for the module-level scope. Without either, assigning inside a function creates a new local variable, which can cause UnboundLocalError if you read it first.
When Python looks up a name it searches Local, then Enclosing function scopes, then Global (module), then Built-in. Closures work through the Enclosing step. The first scope that has the name wins, which is why a local variable hides an outer one.
Lesson: Closures and Captured VariablesUse a closure for a small piece of state and one operation, such as a counter or a configured function (make_discount(10)). Use a class when you need several methods, readable attributes, inheritance or a good __repr__. A class with __call__ is the object version of a closure.
[lambda: i for i in range(3)]?The lambdas share the loop variable i, which is a single variable in the enclosing scope. When they are called later, i is 2. Capturing by default argument (lambda i=i: i) creates a separate value per lambda and returns [0, 1, 2].
A closure keeps the captured variables alive for as long as the function object lives. If it captures a large object, or a reference cycle, that memory stays. Delete the function or avoid capturing big objects when it matters. Normal garbage collection frees the cells when the function is no longer referenced.
Lesson: Closures and Captured VariablesA decorator is a callable that takes a function (or class) and returns a replacement, normally a wrapper that adds behaviour around the original. @deco above a definition is the same as f = deco(f). Typical uses are logging, timing, caching, access checks, retries and registering functions.
Without it the wrapper hides the original: f.__name__ becomes "wrapper", the docstring is lost, and tools such as help, debuggers, logging and frameworks that read function names get wrong information. @wraps(func) copies the name, docstring, module and annotations, and sets __wrapped__ so you can reach the original.
The decorator function runs once, when the decorated function is defined (at import time for module-level code). The wrapper runs every time the decorated name is called. Code that must run per call, such as timing, belongs in the wrapper.
Lesson: Writing Decorators with functools.wrapsThey are applied from the bottom up: @a @b def f is f = a(b(f)). At call time the outermost wrapper (a) runs first, then b's, then the original function, and the return values come back the other way.
Define the wrapper as def wrapper(*args, **kwargs), call func(*args, **kwargs) and return the result. Add @wraps(func) so the metadata is kept. If you need the exact signature in tools, inspect.signature follows __wrapped__ and still shows the original.
Yes. Use a variable in the decorator's closure with nonlocal, a mutable object such as a dictionary, an attribute on the wrapper function, or a class with __call__. The state is shared by every call of that one decorated function but not by other functions.
The wrapper receives self as its first positional argument, which *args passes on. A decorator written with *args, **kwargs works on plain functions and methods without changes. Built-ins such as @property, @staticmethod and @classmethod are decorators too.
Web frameworks register routes (@app.route), check login (@login_required), functools.lru_cache caches results, @dataclass builds methods, @contextmanager creates context managers, and test tools mark tests. They keep cross-cutting concerns like logging and authentication out of business code.
Add an outer function that receives the arguments and returns the decorator: def repeat(times): def decorator(func): @wraps(func) def wrapper(*a, **k): ... return wrapper; return decorator. Then @repeat(3) runs repeat(3) first, and the returned decorator is applied to the function. The arguments live in the closure.
Each lesson has quizzes, practice and more interview questions on the same topic.
Open the first lesson