1. What is the output of the following generator expression execution?
gen = (x**2 for x in range(3))
print(list(gen))
print(list(gen))
Output: [0, 1, 4] followed by []
A generator is a single-use iterator. Unlike a list, it doesn’t store its values — it produces them on demand, one at a time, and remembers its position.
The first list(gen) pulls every value out: 0, 1, 4, building the list [0, 1, 4]. In doing so, it consumes the generator completely.
The second list(gen) finds nothing left. The generator is exhausted, and iterating it yields nothing. Result: [].
This is the core trait of generators (and iterators generally): they are one-shot. You cannot rewind them. If you need the values twice, either create a fresh generator or materialize the result into a list once and reuse the list.
Answer:
[0, 1, 4] followed by []
A generator is a single-use iterator. Unlike a list, it doesn’t store its values — it produces them on demand, one at a time, and remembers its position.
The first list(gen) pulls every value out: 0, 1, 4, building the list [0, 1, 4]. In doing so, it consumes the generator completely.
The second list(gen) finds nothing left. The generator is exhausted, and iterating it yields nothing. Result: [].
This is the core trait of generators (and iterators generally): they are one-shot. You cannot rewind them. If you need the values twice, either create a fresh generator or materialize the result into a list once and reuse the list.
2. What is the output of this dictionary comprehension?
d = {k: v for k, v in enumerate(['a', 'b', 'c'])}
print(d)
Output: {0: 'a', 1: 'b', 2: 'c'}
Two pieces combine here: enumerate and a dict comprehension.
enumerate(['a', 'b', 'c']) yields an index-value pair for each element: (0, 'a'), (1, 'b'), (2, 'c').
The comprehension {k: v for k, v in ...} unpacks each pair and builds a dictionary with k as the key and v as the value. So the keys are the indices 0, 1, 2 and the values are the strings.
Result: {0: 'a', 1: 'b', 2: 'c'}.
The trap in the options is reversing the roles — using the string as the key. Reading the comprehension carefully (k: v, not v: k) settles it. Note this is also exactly what dict(enumerate(['a', 'b', 'c'])) produces, in one less line.
Answer:
{0: 'a', 1: 'b', 2: 'c'}
Two pieces combine here: enumerate and a dict comprehension.
enumerate(['a', 'b', 'c']) yields an index-value pair for each element: (0, 'a'), (1, 'b'), (2, 'c').
The comprehension {k: v for k, v in ...} unpacks each pair and builds a dictionary with k as the key and v as the value. So the keys are the indices 0, 1, 2 and the values are the strings.
Result: {0: 'a', 1: 'b', 2: 'c'}.
The trap in the options is reversing the roles — using the string as the key. Reading the comprehension carefully (k: v, not v: k) settles it. Note this is also exactly what dict(enumerate(['a', 'b', 'c'])) produces, in one less line.
3. What exception is raised when executing next() on an exhausted iterator without a default value?
Answer: StopIteration.
next(iterator) pulls the next item from an iterator. When the iterator has nothing left and you don’t supply a default, Python raises StopIteration to signal “no more values.”
The two-argument form avoids the exception: next(iterator, default) returns default instead of raising when exhausted.
StopIteration sits in a special place: it’s the normal, expected way an iterator signals completion. for loops catch it internally — that’s exactly how they know when to stop. But it’s not treated like a runtime error; it’s part of the iterator protocol.
The interview answer: next() on an exhausted iterator without a default raises StopIteration.
Answer:
StopIteration.
next(iterator) pulls the next item from an iterator. When the iterator has nothing left and you don’t supply a default, Python raises StopIteration to signal “no more values.”
The two-argument form avoids the exception: next(iterator, default) returns default instead of raising when exhausted.
StopIteration sits in a special place: it’s the normal, expected way an iterator signals completion. for loops catch it internally — that’s exactly how they know when to stop. But it’s not treated like a runtime error; it’s part of the iterator protocol.
The interview answer: next() on an exhausted iterator without a default raises StopIteration.
4. What is the result of list(map(lambda x: x * 2, filter(lambda x: x % 2 == 0, [1, 2, 3, 4])))?
Answer: [4, 8]
This is three operations nested, evaluated inside-out.
filter(lambda x: x % 2 == 0, [1, 2, 3, 4]) keeps only the elements where the predicate is true — the even numbers. [2, 4].
Then map(lambda x: x * 2, [2, 4]) doubles each: [4, 8].
The outer list(...) materializes the lazy map result into a concrete list.
Result: [4, 8].
The lesson is mostly about reading nested function calls: filter runs first (it’s innermost), then map over its output. filter selects, map transforms — filter-then-map is the classic pipeline shape.
Answer:
[4, 8]
This is three operations nested, evaluated inside-out.
filter(lambda x: x % 2 == 0, [1, 2, 3, 4]) keeps only the elements where the predicate is true — the even numbers. [2, 4].
Then map(lambda x: x * 2, [2, 4]) doubles each: [4, 8].
The outer list(...) materializes the lazy map result into a concrete list.
Result: [4, 8].
The lesson is mostly about reading nested function calls: filter runs first (it’s innermost), then map over its output. filter selects, map transforms — filter-then-map is the classic pipeline shape.
5. What will [i for i in range(5) if i % 2 == 0 else 0] produce?
Answer: A SyntaxError.
The comprehension as written puts an else at the end, after the for clause — and that’s invalid. A trailing if in a comprehension is a filter: ... if condition keeps only the elements passing the condition. Filters don’t take else.
There are two distinct constructs:
- A filter at the end:
[i for i in range(5) if i % 2 == 0]— yields[0, 2, 4]. - A ternary expression before the
for:[i if i % 2 == 0 else 0 for i in range(5)]— evaluates the ternary for every element, yielding[0, 0, 2, 0, 4].
The question’s code mixes the two, putting an else where only a filter can go. The parser rejects it. The lesson: the if...else ternary lives on the left of for, the if filter lives on the right — and you can’t attach an else to the filter.
Answer:
A SyntaxError.
The comprehension as written puts an else at the end, after the for clause — and that’s invalid. A trailing if in a comprehension is a filter: ... if condition keeps only the elements passing the condition. Filters don’t take else.
There are two distinct constructs:
- A filter at the end:
[i for i in range(5) if i % 2 == 0]— yields[0, 2, 4]. - A ternary expression before the
for:[i if i % 2 == 0 else 0 for i in range(5)]— evaluates the ternary for every element, yielding[0, 0, 2, 0, 4].
The question’s code mixes the two, putting an else where only a filter can go. The parser rejects it. The lesson: the if...else ternary lives on the left of for, the if filter lives on the right — and you can’t attach an else to the filter.
6. What does itertools.chain([1, 2], [3, 4]) return when iterated?
Answer: A single continuous stream of elements: 1, 2, 3, 4.
itertools.chain takes multiple iterables and concatenates them into one iterator. Iterating it produces the elements of the first iterable, then the elements of the second, and so on — as if they were one long sequence.
It’s lazy: it doesn’t build a combined list up front. It pulls from each input in turn. For the inputs [1, 2] and [3, 4], iterating yields 1, 2, 3, 4 in order. Materializing with list(...) gives [1, 2, 3, 4].
This is the efficient way to iterate over many sequences as one — equivalent to itertools.chain.from_iterable(list_of_iterables) when you have the iterables in a collection. The interview answer: chain yields the elements of all inputs concatenated into one stream.
Answer:
A single continuous stream of elements: 1, 2, 3, 4.
itertools.chain takes multiple iterables and concatenates them into one iterator. Iterating it produces the elements of the first iterable, then the elements of the second, and so on — as if they were one long sequence.
It’s lazy: it doesn’t build a combined list up front. It pulls from each input in turn. For the inputs [1, 2] and [3, 4], iterating yields 1, 2, 3, 4 in order. Materializing with list(...) gives [1, 2, 3, 4].
This is the efficient way to iterate over many sequences as one — equivalent to itertools.chain.from_iterable(list_of_iterables) when you have the iterables in a collection. The interview answer: chain yields the elements of all inputs concatenated into one stream.
7. What does the yield from syntax do inside a generator?
Answer: It delegates iteration to a sub-generator or any iterable, yielding all its elements in order.
yield from is a shorthand for “yield everything from this other iterable, one at a time.” For simple cases:
def gen():
yield from [1, 2, 3]
is equivalent to:
def gen():
for x in [1, 2, 3]:
yield x
But yield from is more than a loop, especially for generator delegation. It wires up the full generator protocol: it forwards send(), throw(), and close() from the outer generator to the inner one, and propagates the inner generator’s return value through the yield from expression. This makes it the clean way to compose and reuse generators.
The interview answer: yield from delegates iteration to a sub-generator or iterable, forwarding its elements and the generator control protocol.
Answer:
It delegates iteration to a sub-generator or any iterable, yielding all its elements in order.
yield from is a shorthand for “yield everything from this other iterable, one at a time.” For simple cases:
def gen():
yield from [1, 2, 3]
is equivalent to:
def gen():
for x in [1, 2, 3]:
yield x
But yield from is more than a loop, especially for generator delegation. It wires up the full generator protocol: it forwards send(), throw(), and close() from the outer generator to the inner one, and propagates the inner generator’s return value through the yield from expression. This makes it the clean way to compose and reuse generators.
The interview answer: yield from delegates iteration to a sub-generator or iterable, forwarding its elements and the generator control protocol.
8. What is the output of the following list operation?
lst = [1, 2, 3]
lst.extend("45")
print(lst)
Output: [1, 2, 3, '4', '5']
extend iterates over its argument and appends each element individually. It doesn’t add the argument as a single item.
"45" is a string, and iterating a string yields its characters. So extend("45") appends '4' and then '5' — two separate one-character strings.
The contrast is append: lst.append("45") would add the whole string as a single element, giving [1, 2, 3, '45'].
The distinction in one line: append adds one object; extend adds every element of an iterable. Since a string is iterable, extend unpacks its characters. Result: [1, 2, 3, '4', '5'].
Answer:
[1, 2, 3, '4', '5']
extend iterates over its argument and appends each element individually. It doesn’t add the argument as a single item.
"45" is a string, and iterating a string yields its characters. So extend("45") appends '4' and then '5' — two separate one-character strings.
The contrast is append: lst.append("45") would add the whole string as a single element, giving [1, 2, 3, '45'].
The distinction in one line: append adds one object; extend adds every element of an iterable. Since a string is iterable, extend unpacks its characters. Result: [1, 2, 3, '4', '5'].
9. What is the primary difference between range() in Python 3 and xrange() in Python 2?
Answer: In Python 3, xrange() is gone, and range() became the memory-efficient sequence object — effectively what xrange() was in Python 2.
Python 2 had two functions:
range(n)— eagerly built a list in memory. Fine for small n, wasteful for huge n.xrange(n)— a lazy sequence object that generated values on demand without materializing the whole list.
Python 3 eliminated the duplication: xrange was removed, and range was redefined to be the lazy, memory-efficient object. So in Python 3, range(10**9) uses a constant amount of memory regardless of size.
Note that Python 3’s range is still a sequence — you can index it (r[5]), slice it, and check membership efficiently — it just isn’t a list. The interview answer: Python 3’s range is the lazy, sequence-like object that Python 2 called xrange.
Answer:
In Python 3, xrange() is gone, and range() became the memory-efficient sequence object — effectively what xrange() was in Python 2.
Python 2 had two functions:
range(n)— eagerly built a list in memory. Fine for small n, wasteful for huge n.xrange(n)— a lazy sequence object that generated values on demand without materializing the whole list.
Python 3 eliminated the duplication: xrange was removed, and range was redefined to be the lazy, memory-efficient object. So in Python 3, range(10**9) uses a constant amount of memory regardless of size.
Note that Python 3’s range is still a sequence — you can index it (r[5]), slice it, and check membership efficiently — it just isn’t a list. The interview answer: Python 3’s range is the lazy, sequence-like object that Python 2 called xrange.
Premium Content
Unlock Iterators & Comprehensions and all premium lessons with a subscription.
From ₹199.99/year — See plans