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.
2. What does the @classmethod decorator pass automatically as its first parameter?
Answer: The class object itself, conventionally named cls.
The three method kinds differ in what they bind as the first parameter:
- A normal instance method receives the instance —
self. It can access and modify instance state. - A
@classmethodreceives the class —cls. It doesn’t need an instance to be called;MyClass.my_method()works, and so doesinstance.my_method(). Since it has the class, it can access class attributes and create instances, but it can’t touch instance state. - A
@staticmethodreceives neither — it’s just a plain function sitting inside the class namespace.
The practical difference between classmethod and staticmethod: the class method knows what class it belongs to. That matters for polymorphism — a class method called on a subclass receives the subclass, so a factory like MyClass.from_string(s) will correctly build instances of whatever subclass it’s called on. A static method has no such awareness.
The interview answer: @classmethod passes cls — the class object.
3. What is the output of the following try-except-else-finally block?
def check():
try:
return 1
finally:
return 2
print(check())
Output: 2
The finally block has one unbreakable guarantee: its code always runs before the function actually returns. And if that code contains a return statement, the finally’s return wins.
Execution goes like this: the try block hits return 1. But before the function can hand 1 back to the caller, Python runs the finally block — as it must. The finally contains return 2, which establishes a new return value. That new value is what the function returns.
So the function returns 2, and the return 1 in the try is effectively discarded.
This is a deliberate trap: it looks like 1 is coming out, but a return inside finally overrides any return in the try or except. It’s why linters flag return inside finally — the behavior is nearly always a bug. The interview answer is simply 2.
4. How does copy.deepcopy() differ from copy.copy()?
Answer: copy() makes a shallow copy — a new top-level object whose nested objects are still shared. deepcopy() recursively copies every nested object, so nothing is shared.
The difference shows up with containers inside containers.
A shallow copy, copy.copy(), creates a new container but inserts references to the same child objects. Consider a = [[1, 2], [3, 4]] and b = copy.copy(a). b is a new list, but b[0] is the very same inner list as a[0]. Mutating b[0] changes a[0] too.
A deep copy, copy.deepcopy(), walks the entire structure and duplicates every nested object. b[0] is now an independent list; mutating it leaves a untouched.
The interview answer: shallow = one new layer, inner objects shared; deep = everything duplicated recursively. The choice depends on whether sharing nested objects is acceptable — and deepcopy is also slower, because it does more work.
5. What will be the output of print(“python”[::-1][::1])?
Output: "nohtyp"
This is two slices chained, evaluated left to right.
First, "python"[::-1]. The negative step reverses the string: "nohtyp".
Then the result, "nohtyp"[::1]. A step of 1 moves forward through every character in order — the identity slice. It changes nothing.
So the chain reduces to just the reversal: "nohtyp".
The lesson: [::-1] reverses; [::1] is a no-op. Chaining them in that order leaves you with a reversed string. If the order were flipped ([::1][::-1]), you’d also get "nohtyp" — because reversal is self-inverse and the identity slice is harmless.
6. Which of the following data structures allows O(1) average time complexity for checking element membership?
Answer: set.
The membership check x in container has different costs per structure:
listandtuple— stored in order, no hash index. Membership requires a linear scan: O(n).set— backed by a hash table. Each element is hashed to a bucket, sox in my_setis O(1) on average (a fast constant-time probe).deque— a double-ended queue; also O(n) for membership, because it has no hash index.
The trade-off: a set is unordered and holds only unique elements. When you need fast membership and order doesn’t matter, a set is the right tool. (A dict is the same hash-table story, with keys instead of elements.)
The interview answer: set, whose hash-based membership is O(1) on average.
7. 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.
8. 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.
9. What will print(1 or 2) and print(1 and 2) display?
Output: 1 and 2
Python’s or and and don’t return booleans — they return one of their operands, using short-circuit evaluation.
1 or 2:orreturns the first truthy operand.1is truthy, so evaluation stops right there and1is returned. Result:1.1 and 2:andreturns the first falsy operand, or the last operand if all are truthy.1is truthy, so it continues to2.2is the last operand, so it’s returned. Result:2.
The general rules:
a or b→aifais truthy, elseb.a and b→aifais falsy, elseb.
This is why or is used for defaults — name = user_input or "default" — and and for guarding. The values come back untouched, not coerced to True/False. That’s the distinction this question tests.
10. What is the purpose of the slots declaration in a Python class?
Answer: __slots__ optimizes memory by preventing the automatic creation of a per-instance __dict__, fixing the set of allowed attributes in advance.
Normally every Python object carries an instance dictionary, __dict__, which stores its attributes. That dict is flexible but heavy — it’s a whole hash table per instance. When you’re creating millions of instances (say, in data processing), that overhead dominates memory.
Declaring __slots__ = ('x', 'y') tells Python: instances may only have these attributes, and they will be stored in compact internal descriptors instead of a __dict__. The instance dict is never created, so each object gets dramatically smaller.
Two consequences follow:
- Memory savings — the headline benefit; can be substantial at scale.
- Restricted attributes — you can no longer assign arbitrary attributes;
obj.z = 1raisesAttributeErrorwhenzisn’t in__slots__. That’s usually fine, since you declared what you need.
The trade-off is flexibility for memory. The interview answer: __slots__ removes the per-instance __dict__ to save memory, at the cost of a fixed attribute set.
11. 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.
12. What happens when key modification occurs on a dictionary while iterating over it directly?
Answer: Python raises RuntimeError: dictionary changed size during iteration.
Direct iteration over a dict — for k in d: — is sensitive to the dictionary’s size. If you add or delete keys while iterating, the dict’s internal structure changes in a way the iterator can’t safely track. Python detects this and raises RuntimeError rather than producing unpredictable results.
Two details matter:
- Changing values is fine —
d[k] = new_valuedoesn’t change the set of keys, so iteration proceeds normally. - Adding or removing keys is the problem. The safe pattern is to iterate over a snapshot of the keys —
for k in list(d):— and modify the original dict inside the loop.
The interview answer: adding or deleting keys during direct dict iteration raises RuntimeError: dictionary changed size during iteration.
13. What is the output of the following code?
def func(a, b=5, *args, **kwargs):
print(len(args), len(kwargs))
func(1, 2, 3, 4, x=5, y=6)
Output: 2 2
Trace the argument binding carefully.
a = 1— first positional.b = 5is the default, but the call passes2as the second positional, sob = 2.*argscollects the remaining positional arguments —3and4. That’s 2 items.**kwargscollects all keyword arguments not matched by named parameters —x=5andy=6. That’s 2 items.
So len(args) is 2 and len(kwargs) is 2. Output: 2 2.
The trap is counting wrong: a and b soak up the first two positionals, leaving (3, 4) for args, while the two keyword arguments x and y go to kwargs. Both lengths are 2.
14. How does isinstance(True, int) evaluate in Python?
Answer: True.
In Python, bool is a subclass of int. The two boolean values are just special integers: True == 1 and False == 0.
isinstance(obj, type) checks whether the object’s type is the given type or any of its subclasses. Since bool subclasses int, isinstance(True, int) is True.
This has real, occasionally surprising consequences:
True + Truegives2.["a", "b"][True]gives"b"(index 1).sum([True, False, True])gives2— a handy trick for counting.
Note the asymmetry: isinstance(True, int) is True, but the reverse direction — isinstance(1, bool) — is False. An int is not necessarily a bool; bools are a narrower type on top of ints. The interview answer is simply True, because bool inherits from int.
15. What does the zip() function do when passed sequences of unequal lengths?
Answer: By default, zip() truncates — it stops as soon as the shortest input is exhausted.
zip(a, b, ...) pairs up the elements position by position: the first element of each, then the second of each, and so on. When one sequence runs out, there’s nothing left to pair, so zip simply stops.
zip([1, 2, 3], "ab") # yields (1, 'a'), (2, 'b') — stops there
The unpaired 3 is dropped silently. This default is usually what you want, but it can hide bugs when you expect equal lengths.
Python 3.10 added strict=True as an opt-in safety net: zip(a, b, strict=True) raises ValueError if the lengths differ, which catches mismatched data instead of silently truncating.
The interview answer: default behavior is truncation at the shortest input. strict=True (3.10+) turns a length mismatch into an error.
Premium Content
Unlock Top 25 - Part 1 and all premium lessons with a subscription.
From ₹199.99/year — See plans