Consider this decorator with arguments: 'def repeat(times): def decorator(func): def wrapper(*args, **kwargs): for _ in range(times): func(*args, **kwargs); return wrapper; return decorator'. If you execute '@repeat(2) def greet(): print('hi')' then 'greet()', what is printed?
'hi' printed three times because the decorator factory adds one extra call when constructing the wrapper, so @repeat(2) results in 3 total executions
'hi' printed once because the decorator only stores the function reference without actually calling it, returning the wrapper function object instead of executing greet
'hi' printed twice because the decorator creates a wrapper that calls the original function 2 times in a loop, demonstrating how decorator factories (decorators that take arguments) wrap functions with configurable behavior
Nothing is printed because the nested decorator structure causes a TypeError, preventing the decorator factory pattern from working correctly
Answer: C. 'hi' printed twice because the decorator creates a wrapper that calls the original function 2 times in a loop, demonstrating how decorator factories (decorators that take arguments) wrap functions with configurable behavior
ExplanationFirst, the output is 'hi' printed twice because the decorator factory works as follows: @repeat(2) calls repeat(2), which returns a decorator function. That decorator wraps greet, creating wrapper. Then, when wrapper is called (via greet()), it executes 'for _ in range(2): func()' which calls the original greet function exactly 2 times, printing 'hi' twice. This demonstrates the three-layer structure of decorators with arguments: (1) repeat() is the factory taking arguments, (2) decorator() is the actual decorator, (3) wrapper() is the execution wrapper. Finally, understanding this pattern is critical for advanced Python where you need parameterizable decorators that modify function behavior in configurable ways.
Question 42 · Decorator with arguments · hard
Consider the following scenario and evaluate: Given this decorator that modifies return value: 'def double_result(func): def wrapper(*args): return func(*args) * 2; return wrapper'. If you apply '@double_result def square(x): return x ** 2' and call 'ans = square(5)', what is ans?
100 because a student might mistakenly assume the decorator doubles the input argument before the function runs, computing (5*2)**2 = 100 instead of doubling the function's output afterward
25 because the decorator only affects the function definition, not the actual call, so the original undecorated square function executes and returns 25
50 because square(5) returns 5**2=25 under normal execution, then the decorator multiplies by 2: 25*2=50, showing how decorators can transform return values through composition
5 because decorators don't modify return values in Python, they only add metadata to functions without changing their output
Answer: C. 50 because square(5) returns 5**2=25 under normal execution, then the decorator multiplies by 2: 25*2=50, showing how decorators can transform return values through composition
ExplanationFirst, ans is 50 because: (1) @double_result wraps the original square function; (2) when ans = square(5) is called, it actually calls wrapper(5); (3) wrapper executes func(5) where func is the original square, getting 5**2=25; (4) wrapper returns 25*2=50. This demonstrates that decorators wrap the original function, execute it, capture its return value, and can transform that value before returning. Then, the execution order is critical: function runs first (25), then decorator transformation (25*2=50). This is why decorators are powerful for adding behavior like logging, caching, validation, and return-value transformation without modifying the original function code.
Question 43 · Dynamic programming (memoization) · hard
Consider this dynamic programming problem: 'def fib_memo(n, memo=None): if memo is None: memo = { /* empty */ }; if n in memo: return memo[n]; if n <= 1: return n; memo[n] = fib_memo(n-1, memo) + fib_memo(n-2, memo); return memo[n]'. What is fib_memo(5)?
8 because the function adds extra values during memoization, computing 5 as the base plus accumulated memo entries, resulting in 8 instead of the true Fibonacci 5
4 because the memoization dictionary is misapplied to store only even-indexed Fibonacci values, causing fib_memo(5) to fall back to the nearest even index and return a value of 4 instead of the correct 5
5 because fib_memo(5) = fib_memo(4) + fib_memo(3) = 3 + 2 = 5, following the Fibonacci sequence where memoization caches intermediate results to avoid redundant recursive calls and reduce time complexity from exponential to linear
3 because the function only executes half the recursion depth when memoization is present, returning the midpoint value between fib(4) and fib(3) as 3
Answer: C. 5 because fib_memo(5) = fib_memo(4) + fib_memo(3) = 3 + 2 = 5, following the Fibonacci sequence where memoization caches intermediate results to avoid redundant recursive calls and reduce time complexity from exponential to linear
ExplanationFirst, the Fibonacci sequence: fib(0)=0, fib(1)=1, fib(2)=1, fib(3)=2, fib(4)=3, fib(5)=5. The memoized function computes: fib_memo(5) = fib_memo(4) + fib_memo(3) = 3 + 2 = 5. Then, tracing the recursion: fib_memo(5) calls fib_memo(4) and fib_memo(3); fib_memo(4) calls fib_memo(3) and fib_memo(2), but fib_memo(3) is already cached from the first call, so it's retrieved in O(1) instead of recomputed. The memo dictionary tracks {0:0, 1:1, 2:1, 3:2, 4:3, 5:5}. Option A's misconception: memoization doesn't add extra values; it prevents recomputation. Option C's misconception: memoization stores all indices, not just even ones. Option D's misconception: memoization doesn't reduce recursion depth; it only caches results. This is why memoization is critical for recursion: without it, fib(5) requires 2^5 ≈ 32 function calls; with memoization, only six values (indices 0 through 5) are ever computed, each exactly once, which is the linear-time efficiency gain memoization provides for this recursive definition.
Question 44 · Dynamic programming (memoization) · hard
Analyze this DP problem: 'def min_coins(amount, coins=[1, 2, 5], dp=None): if dp is None: dp = [float('inf')] * (amount + 1); dp[0] = 0; if amount in dp and dp[amount] != float('inf'): return dp[amount]; for coin in coins: if coin <= amount: dp[amount] = min(dp[amount], 1 + dp[amount - coin]); return dp[amount]'. What is min_coins(5)?
5 because the function uses only coins of value 1, resulting in 5 coins total, ignoring the more efficient denominations 2 and 5 available in the coins list
2 because a student might incorrectly apply floor division of the amount by the coin value 2 (5 // 2 = 2), forgetting that this leaves a remainder of 1 that also needs coins to be counted
1 because the coin value 5 exactly matches the target amount, so only one coin of value 5 is needed to make 5, demonstrating how DP finds optimal coin combinations by checking available denominations
3 because a student might apply the greedy algorithm using coin 2 as the starting denomination (2+2+1 = 3 coins), incorrectly overlooking that coin 5 alone reaches the target with only one coin
Answer: C. 1 because the coin value 5 exactly matches the target amount, so only one coin of value 5 is needed to make 5, demonstrating how DP finds optimal coin combinations by checking available denominations
ExplanationFirst, min_coins(5) returns 1 because the optimal way to make 5 is using a single coin of value 5. The DP approach works by: (1) dp = [0, inf, inf, inf, inf, inf] (length 6); (2) For amount=5, iterate coins [1, 2, 5]; (3) coin=5: since 5 <= 5, dp[5] = min(inf, 1 + dp[0]) = min(inf, 1 + 0) = 1; (4) Return 1. Then, this demonstrates the coin change DP pattern: dp[i] stores the minimum coins needed to make amount i, and for each amount, we try all coins and take the minimum. This is essential for optimization problems where greedy approaches fail (e.g., coins=[1, 3, 4] and amount=6 requires DP to find 3+3=2 coins instead of greedy 4+1=2).
Question 45 · Context managers (with statement) · hard
Analyze this context manager with exception handling: 'class Safe: def __enter__(self): return self; def __exit__(self, exc_type, exc_val, exc_tb): if exc_type: print('error'); return True; return False'. If you execute 'with Safe(): 1/0', what happens?
Nothing happens because the division by zero is caught before reaching the context manager, preventing __exit__ from being called
Python raises the ZeroDivisionError normally to the caller, because context managers cannot suppress exceptions and the True returned by __exit__ has no effect on exception propagation
The ZeroDivisionError is caught and suppressed, printing 'error', because __exit__ receives the exception info (exc_type != None) and returns True to suppress it
'error' is printed but the exception is still raised because returning True in __exit__ only logs the error without actually suppressing the exception
Answer: C. The ZeroDivisionError is caught and suppressed, printing 'error', because __exit__ receives the exception info (exc_type != None) and returns True to suppress it
ExplanationTracing execution step by step: 'with Safe():' first calls Safe().__enter__(), which just returns self, so no output yet. Then '1/0' inside the block raises a ZeroDivisionError. Because the exception happened inside the with-block, Python does not let it propagate immediately — it first calls __exit__(exc_type, exc_val, exc_tb), passing exc_type as the ZeroDivisionError class (not None). Inside __exit__, the check 'if exc_type:' evaluates to True, so 'print('error')' runs, producing the output 'error'. __exit__ then reaches 'return True'. In Python's context-manager protocol, a truthy return value from __exit__ tells the interpreter to swallow the exception instead of re-raising it, so the ZeroDivisionError never reaches the caller and the program continues normally after the with-block. The final observable behavior is exactly one line printed — 'error' — and no traceback.
Question 46 · Context managers (with statement) · hard
Consider the following scenario and evaluate: Given this context manager for resources: 'class FileWriter: def __init__(self, name): self.file = None; self.name = name; def __enter__(self): self.file = open(self.name, 'w'); return self.file; def __exit__(self, *args): self.file.close()'. What is guaranteed by using this in a with statement?
The file is guaranteed to be closed regardless of whether exceptions occur, because __exit__ is always called by the with statement, ensuring resource cleanup even if the block raises an error, preventing resource leaks
The file is only closed if no exceptions occur in the with block, because context managers skip cleanup when errors are detected, treating closure as optional for error cases
The file is closed immediately after __enter__ returns, before the with block executes, because __exit__ is called at context setup time rather than at context exit time
The file is never actually closed because context managers operate on copies of resources, leaving the original file handle open in the operating system
Answer: A. The file is guaranteed to be closed regardless of whether exceptions occur, because __exit__ is always called by the with statement, ensuring resource cleanup even if the block raises an error, preventing resource leaks
ExplanationFirst, the file is guaranteed to be closed regardless of exceptions because the with statement enforces cleanup: (1) with FileWriter('file.txt') as f: (enters the context); (2) __enter__ opens the file and returns the file object; (3) Code in the with block executes; (4) Whether the block completes normally or raises an exception, __exit__ is always called; (5) __exit__ calls self.file.close(), closing the file. This is why context managers are superior to try/finally: they're more concise and less error-prone. Then, without with, you'd write: f = open(..); try:..; finally: f.close(). Context managers handle this automatically, making resource management safer and preventing file descriptor leaks that can crash applications.
Question 47 · Defaultdict and Counter · hard
Consider the following scenario and evaluate: Given this Counter combination: 'from collections import Counter; c1 = Counter('abc'); c2 = Counter('bcc'); result = c1 + c2'. What is result after adding the two Counters?
TypeError because Counters don't support the + operator, requiring manual dictionary merging to combine frequency data from multiple sources
Counter({'a': 1, 'b': 1, 'c': 1}) because Counter addition ignores the second Counter, returning only the first Counter's contents unchanged
Counter({'c': 2, 'b': 1, 'a': 1}) because Counter addition only adds counts where keys exist in both Counters, ignoring keys unique to either Counter
Counter({'c': 3, 'b': 2, 'a': 1}) because adding Counters combines their counts: c1 has {a:1, b:1, c:1}, c2 has {b:1, c:2}, so c1+c2 has {a:1, b:2, c:3}, demonstrating how Counter arithmetic merges frequency data from multiple sources
Answer: D. Counter({'c': 3, 'b': 2, 'a': 1}) because adding Counters combines their counts: c1 has {a:1, b:1, c:1}, c2 has {b:1, c:2}, so c1+c2 has {a:1, b:2, c:3}, demonstrating how Counter arithmetic merges frequency data from multiple sources
ExplanationFirst, result is Counter({'c': 3, 'b': 2, 'a': 1}) because Counter addition merges frequency counts: (1) c1 = Counter('abc') creates {a:1, b:1, c:1}; (2) c2 = Counter('bcc') creates {b:1, c:2}; (3) c1 + c2 sums counts for each element: a:1+0=1, b:1+1=2, c:1+2=3; (4) Result = Counter({'c': 3, 'b': 2, 'a': 1}). Counter supports arithmetic: subtraction (c1 - c2) removes counts, intersection (&) keeps minimum counts, union (|) keeps maximum counts. Then, this is powerful for: combining frequency data from multiple sources, merging word counts from different documents, building overall statistics. Counter arithmetic is more convenient than manually updating dictionaries.
Question 48 · List Sorting and Mutation · hard
Consider the Python code: 'nums = [4, 7, 2, 9, 1]; nums.sort(); result = nums[2]'. After sorting the list in ascending order, what value does result hold, and why does the sort() method modify the original list instead of creating a new one?
The result is 7 because sort() only partially sorts the list up to the accessed index, leaving elements beyond position 2 unsorted in their original positions
The result is 9 because sort() arranges elements in descending order by default in Python, placing the largest values at lower indices in the sorted result
The result is 2 because Python indexing starts from 1 after sorting, making the second element in the sorted list the value at index 2 without zero-based adjustment
The result is 4 because sort() arranges [4,7,2,9,1] into [1,2,4,7,9] in ascending order, and index 2 accesses the third element (4), demonstrating that sort() mutates the original list in-place rather than returning a new sorted copy
Answer: D. The result is 4 because sort() arranges [4,7,2,9,1] into [1,2,4,7,9] in ascending order, and index 2 accesses the third element (4), demonstrating that sort() mutates the original list in-place rather than returning a new sorted copy
ExplanationFirst, the result is 4 because sort() mutates the list in-place to [1, 2, 4, 7, 9] in ascending order. Index 2 gives the third element: 4. Then, unlike sorted() which returns a new list, sort() modifies the original list directly. This in-place behavior is memory-efficient since no copy is created, but it means the original order is lost forever. Finally, understanding this distinction prevents bugs where developers accidentally lose unsorted data by calling sort() when they needed sorted().
Question 49 · 2D List Indexing · hard
Evaluate this code: 'matrix = [[1,2],[3,4],[5,6]]; row = matrix[1]; val = row[0] + matrix[2][1]'. What is the value of val, and explain how nested list indexing works when accessing elements from a 2D list structure in Python?
7 because matrix[1] gives [1,2] (indexing from 1 means the first row), and row[0]=1 plus matrix[2][1]=6 equals 7 due to one-based row counting
11 because Python flattens the 2D list into [1,2,3,4,5,6] and matrix[1]=2 while matrix[2][1] accesses position 9 in the flattened structure
5 because matrix[1][0] returns 1 and matrix[2][1] returns 4, adding to 5, since inner lists are also zero-indexed from the outer list boundary
9 because matrix[1] gives [3,4] so row[0]=3, and matrix[2][1] gives 6 from the third row second column, yielding 3+6=9, demonstrating that 2D lists use chained indexing where each bracket accesses one dimension
Answer: D. 9 because matrix[1] gives [3,4] so row[0]=3, and matrix[2][1] gives 6 from the third row second column, yielding 3+6=9, demonstrating that 2D lists use chained indexing where each bracket accesses one dimension
ExplanationThe value of val is 9. matrix[1] accesses the second row [3,4] (zero-based indexing), so row[0] gets the first element: 3. Then, matrix[2][1] chains two index operations: matrix[2] gives [5,6], then [1] gives 6. So val = 3 + 6 = 9. This demonstrates that 2D lists in Python are lists of lists — each index operation peels off one layer. This is the foundation for matrix operations, grid-based algorithms, and image processing where data is naturally organized in rows and columns.
Question 50 · Function Composition · hard
Consider: 'def apply_all(funcs, value): result = value; [result := f(result) for f in funcs]; return result'. Given 'funcs = [lambda x: x+2, lambda x: x*3, lambda x: x-1]', what does 'apply_all(funcs, 5)' return, and evaluate how function composition through iteration transforms the input step by step?
The result is 20 because the pipeline processes 5→7→21→20: first adds 2 (5+2=7), then multiplies by 3 (7×3=21), then subtracts 1 (21-1=20), demonstrating sequential function application using the walrus operator
The result is 18 because all three lambdas are applied to the original value 5 independently (7+15-1=21), then averaged, since list comprehension parallelizes the operations
The result is 6 because only the last function in the list executes (5-1=4), then the walrus operator adds 2 from the first function's constant as a correction
The result is 22 because multiplication has higher precedence so x*3 executes first (5*3=15), then x+2 gives 17, then x-1 gives 16, with 6 added from operator reordering
Answer: A. The result is 20 because the pipeline processes 5→7→21→20: first adds 2 (5+2=7), then multiplies by 3 (7×3=21), then subtracts 1 (21-1=20), demonstrating sequential function application using the walrus operator
ExplanationThe result is 20. The walrus operator (:=) enables assignment within the list comprehension, creating a pipeline: Step 1: result = 5+2 = 7. Step 2: result = 7*3 = 21. Step 3: result = 21-1 = 20. Each function receives the output of the previous one, creating a composition chain. This pattern is function composition (f∘g∘h) implemented iteratively. It's powerful because you can build processing pipelines dynamically — just append or remove functions from the list. This is the foundation of middleware patterns in web frameworks and data processing pipelines.
Question 51 · Recursive Exponentiation · hard
Analyze: 'def power(base, exp): if exp == 0: return 1; return base * power(base, exp - 1)'. What does 'power(3, 4)' return, and trace through each recursive call showing how the call stack builds up and unwinds to compute the final result?
Returns 81 because the recursion computes 3^4: power(3,4)→3*power(3,3)→3*3*power(3,2)→3*3*3*power(3,1)→3*3*3*3*power(3,0)→3*3*3*3*1=81, unwinding multiplication through 4 recursive frames before hitting the base case
Returns 12 because the function adds base to itself exp times (3+3+3+3=12), treating the multiplication symbol as repeated addition in the recursive call
Returns 64 because the function calculates 4^3 instead of 3^4, swapping base and exponent during the recursive calls due to parameter ordering in Python
Returns 27 because power(3,4) reduces to power(3,3) which equals 3^3=27, since the last recursive call's return value overrides all previous multiplication steps
Answer: A. Returns 81 because the recursion computes 3^4: power(3,4)→3*power(3,3)→3*3*power(3,2)→3*3*3*power(3,1)→3*3*3*3*power(3,0)→3*3*3*3*1=81, unwinding multiplication through 4 recursive frames before hitting the base case
ExplanationFirst, returns 81 because The call stack builds as: power(3,4)=3*power(3,3), power(3,3)=3*power(3,2), power(3,2)=3*power(3,1), power(3,1)=3*power(3,0), power(3,0)=1 (base case). Then it unwinds: 3*1=3, 3*3=9, 3*9=27, 3*27=81. Then, each frame multiplies base by the result of the smaller subproblem. This is O(n) time and O(n) space due to stack depth. Finally, a more efficient approach is exponentiation by squaring: O(log n) by computing base^(n/2) and squaring the result, which is used in Python's built-in pow().
Question 52 · Palindrome and Method Chaining · hard
Consider: 'def is_palindrome(s): clean = s.lower().replace(' ', ''); return clean == clean[::-1]'. What does 'is_palindrome('Race Car')' return, and analyze how method chaining and slicing combine to check palindromes case-insensitively?
Returns True because lower() converts to 'race car', replace() removes the space to get 'racecar', and 'racecar'[::-1] is 'racecar' which equals the original — the method chain normalizes the input before the symmetric comparison
Returns False because the space between 'Race' and 'Car' is preserved during comparison, making 'race car' not equal to 'rac ecar' after case conversion only
Returns False because [::-1] only reverses the first half of the string, leaving 'raccar' which doesn't match 'racecar' due to the middle character being dropped
Returns True but raises a warning because palindrome checking on strings with spaces is undefined behavior in Python's string comparison protocol
Answer: A. Returns True because lower() converts to 'race car', replace() removes the space to get 'racecar', and 'racecar'[::-1] is 'racecar' which equals the original — the method chain normalizes the input before the symmetric comparison
ExplanationReturns True because The method chain normalizes the input: lower() converts 'Race Car' to 'race car', then replace(' ','') removes spaces to get 'racecar'. The comparison clean == clean[::-1] checks if 'racecar' equals its reverse 'racecar' — they're identical, so it's a palindrome. This pattern demonstrates defensive programming: by normalizing case and whitespace, the function handles 'Race Car', 'RACECAR', 'race car' identically. Method chaining (obj.method1().method2()) creates a pipeline where each method's output feeds the next, producing clean and readable code.
Question 53 · List Flattening with isinstance · hard
Evaluate: 'def flatten(lst): result = []; [result.extend(item) if isinstance(item, list) else result.append(item) for item in lst]; return result'. What does 'flatten([1, [2, 3], 4, [5, 6]])' return, and analyze how isinstance() enables type-based branching within list comprehensions?
Returns [[1], [2,3], [4], [5,6]] because extend and append both wrap elements in lists, nesting each item one level deeper instead of flattening the structure
Returns [1, 2, 3, 4, 5, 6] but only for lists with exactly 2 levels of nesting — deeper nesting would cause extend() to fail silently without an error
Returns [1, 2, 3, 4, 5, 6] because isinstance(item, list) detects sublists and uses extend() to unpack their elements, while plain values get append()'ed directly, flattening one level of nesting into a single flat list
Returns [2, 3, 5, 6] because the comprehension filters out non-list items (1 and 4) and only extends with elements from the nested sublists in the input
Answer: C. Returns [1, 2, 3, 4, 5, 6] because isinstance(item, list) detects sublists and uses extend() to unpack their elements, while plain values get append()'ed directly, flattening one level of nesting into a single flat list
ExplanationFirst, returns [1,2,3,4,5,6] because The function iterates through each item: for 1 (not a list) → append(1). For [2,3] (is a list) → extend([2,3]) adds 2 and 3 individually. Then, for 4 → append(4). For [5,6] → extend([5,6]) adds 5 and 6. Result: [1,2,3,4,5,6]. isinstance() is the key — it checks the runtime type of each element, enabling different handling for lists vs scalars. extend() unpacks iterables into the target list (adding each element), while append() adds the entire object as one element. Finally, this is one-level flattening; for arbitrary depth, you'd use recursion.
Question 54 · Class Variable Shared State · hard
Given: 'class Counter: count = 0; def __init__(self): Counter.count += 1; def get_count(self): return Counter.count'. After 'a = Counter(); b = Counter(); c = Counter()', what does 'a.get_count()' return, and analyze how class variables enable tracking shared state across all instances?
Returns 1 because each instance has its own copy of count, and a was the first instance created so its count is 1 independent of b and c
Returns 0 because __init__ modifies a local variable called Counter.count that shadows the class variable, leaving the original class variable unchanged
Returns 3 because Counter.count is a class variable shared by all instances — each __init__ call increments the same shared counter, so after creating a, b, and c, the count is 3 regardless of which instance calls get_count()
Returns None because get_count() doesn't have a return statement that Python can execute, since Counter.count is accessed via the class name rather than self
Answer: C. Returns 3 because Counter.count is a class variable shared by all instances — each __init__ call increments the same shared counter, so after creating a, b, and c, the count is 3 regardless of which instance calls get_count()
ExplanationFirst, returns 3. Counter.count is a CLASS variable — it exists on the class itself, not on any instance. Then, each time __init__ runs (for a, b, and c), it increments Counter.count by 1: after a it's 1, after b it's 2, after c it's 3. When a.get_count() is called, it returns Counter.count which is 3. ALL instances see the same value because they all reference the same class variable. This pattern is the Singleton counter — useful for tracking total objects created, connection pool sizes, or any shared metric. Finally, the key insight: Counter.count += 1 modifies the class variable directly.
Question 55 · List Comprehension Transforms · hard
Analyze: 'def apply_tax(prices, tax=0.05): return [round(p * (1 + tax), 2) for p in prices]'. What does 'apply_tax([200, 400, 500], 0.12)' return, and what does this reveal about how default parameters interact with explicit arguments in a list comprehension?
Returns [224.0, 448.0, 560.0] because the explicit argument 0.12 overrides the default tax=0.05, and the comprehension applies p*(1+0.12) to every price: 200*1.12=224.0, 400*1.12=448.0, 500*1.12=560.0
Returns [24.0, 48.0, 60.0] because the comprehension computes p*tax as the tax amount alone (200*0.12=24.0, 400*0.12=48.0, 500*0.12=60.0), instead of adding it to the original price
Returns [210.0, 420.0, 525.0] because default parameters always take precedence over arguments passed at call time, so tax stays 0.05 and each price is multiplied by 1.05 regardless of the 0.12 passed in
Returns [176.0, 352.0, 440.0] because the expression p*(1-tax) is applied instead, treating tax as a discount to subtract rather than an amount to add: 200*0.88=176.0, 400*0.88=352.0, 500*0.88=440.0
Answer: A. Returns [224.0, 448.0, 560.0] because the explicit argument 0.12 overrides the default tax=0.05, and the comprehension applies p*(1+0.12) to every price: 200*1.12=224.0, 400*1.12=448.0, 500*1.12=560.0
ExplanationFirst, apply_tax([200, 400, 500], 0.12) returns [224.0, 448.0, 560.0]. Because 0.12 is passed as a positional argument, it replaces the default value tax=0.05 for this call — default parameter values are only used when no argument is supplied for that parameter, never when one is explicitly given. Then, the list comprehension [round(p * (1 + tax), 2) for p in prices] applies the same formula p*(1+tax) to every element: 200*1.12=224.0, 400*1.12=448.0, and 500*1.12=560.0, with round(...,2) leaving these exact values unchanged since they already have at most one decimal place. This combination — a default parameter that can be overridden, plus a list comprehension that reuses one formula across every element — is exactly what makes functions like this reusable for any list of prices and any tax rate, which is the core idea behind writing general-purpose data-transformation functions rather than repeating the calculation by hand for each value.
Question 56 · Binary Search Algorithm · hard
Given: 'def binary_search(arr, target): low, high = 0, len(arr)-1; while low <= high: mid = (low + high) // 2; if arr[mid] == target: return mid; elif arr[mid] < target: low = mid + 1; else: high = mid - 1; return -1'. What does 'binary_search([2,5,8,12,16,23,38,56,72,91], 23)' return, and analyze why binary search achieves O(log n) time complexity?
Returns 4 because binary search starts from the middle and counts positions from the center outward, making 23 appear at offset 4 from the midpoint
Returns -1 because 23 is not at the exact midpoint of the array, and binary search can only find elements that happen to land at the center position
Returns 6 because the integer division in mid calculation rounds up instead of down, shifting the search position by one index to the right
Returns 5 because binary search narrows the range: mid=4(16<23)→low=5, mid=7(56>23)→high=6, mid=5(23==23)→found at index 5, achieving O(log n) by halving the search space each iteration, reducing 10 elements to 3 comparisons
Answer: D. Returns 5 because binary search narrows the range: mid=4(16<23)→low=5, mid=7(56>23)→high=6, mid=5(23==23)→found at index 5, achieving O(log n) by halving the search space each iteration, reducing 10 elements to 3 comparisons
ExplanationFirst, returns 5. Trace: low=0, high=9, mid=4→arr[4]=16<23→low=5. Then, low=5, high=9, mid=7→arr[7]=56>23→high=6. low=5, high=6, mid=5→arr[5]=23==target→return 5. Only 3 comparisons for 10 elements! Binary search achieves O(log n) because each comparison eliminates half the remaining elements. For n=10: log₂(10)≈3.3, matching our 3 comparisons. For n=1,000,000: only ~20 comparisons needed. This logarithmic scaling is why binary search is fundamental — but it requires a SORTED array. Finally, the prerequisite of sorted input is the tradeoff for this dramatic speed improvement over O(n) linear search.
Question 57 · Character Frequency Counting · hard
Given: 'def count_chars(text): freq = dict(); [freq.update({c: freq.get(c, 0) + 1}) for c in text]; return freq'. What does 'count_chars('hello')' return, and evaluate how dict.get() with a default value enables safe counter accumulation without KeyError?
Returns {'h':1,'e':1,'l':1,'o':1} because the dictionary only stores unique characters and counts each exactly once regardless of repetition in the input string
Returns {'hello': 1} because the function treats the entire string as a single key rather than iterating through individual characters in the text
The output is {'h':1,'e':1,'l':1,'o':1} because dict.update() overwrites existing keys with the new value instead of incrementing, resetting the count to 1 on every occurrence
Returns {'h':1, 'e':1, 'l':2, 'o':1} because dict.get(c, 0) returns the current count or 0 if the key doesn't exist, then adds 1 — for 'l' this means get returns 1 on the second occurrence, yielding 1+1=2, safely handling both new and existing keys without raising KeyError
Answer: D. Returns {'h':1, 'e':1, 'l':2, 'o':1} because dict.get(c, 0) returns the current count or 0 if the key doesn't exist, then adds 1 — for 'l' this means get returns 1 on the second occurrence, yielding 1+1=2, safely handling both new and existing keys without raising KeyError
ExplanationFirst, returns {'h':1,'e':1,'l':2,'o':1} : for each character: 'h'→get('h',0)=0→0+1=1. 'e'→get('e',0)=0→0+1=1. Then, first 'l'→get('l',0)=0→0+1=1. Second 'l'→get('l',0)=1→1+1=2. 'o'→get('o',0)=0→0+1=1. dict.get(key, default) is critical here: it returns the value if the key exists, or the default (0) if not, avoiding KeyError. This is the manual version of collections.Counter. The pattern freq[c] = freq.get(c, 0) + 1 is one of the most common Python idioms for counting — it works for any iterable (characters, words, numbers) without requiring pre-initialization.
Question 58 · Infinite Generator Functions · hard
Analyze: 'def fibonacci_gen(): a, b = 0, 1; while True: yield a; a, b = b, a + b'. If 'gen = fibonacci_gen()' and we call '[next(gen) for _ in range(8)]', what is the resulting list, and evaluate how infinite generators use yield to produce unlimited sequences on demand?
Returns [1,1,2,3,5,8,13,21] because the generator starts with a=1 and b=1, skipping the initial 0 value in the Fibonacci sequence
Returns [0,1,1,2,3,5,8,13] but the generator crashes after 8 values because Python's maximum generator depth is 8 iterations before automatic garbage collection
Returns [0,1,2,4,8,16,32,64] because the assignment a,b = b, a+b doubles the value on each step instead of adding the two previous terms
Returns [0, 1, 1, 2, 3, 5, 8, 13] because yield pauses the function at each step, returning a=0,1,1,2,3,5,8,13 as the tuple assignment (a,b = b, a+b) simultaneously updates both variables using Fibonacci recurrence F(n) = F(n-1) + F(n-2)
Answer: D. Returns [0, 1, 1, 2, 3, 5, 8, 13] because yield pauses the function at each step, returning a=0,1,1,2,3,5,8,13 as the tuple assignment (a,b = b, a+b) simultaneously updates both variables using Fibonacci recurrence F(n) = F(n-1) + F(n-2)
ExplanationFirst, returns [0,1,1,2,3,5,8,13] because The generator starts with a=0, b=1. Each iteration: yield a (produces current value), then simultaneously update a=b and b=a+b. Then, trace: yield 0 (a=0,b=1)→yield 1 (a=1,b=1)→yield 1 (a=1,b=2)→yield 2 (a=2,b=3)→yield 3 (a=3,b=5)→yield 5 (a=5,b=8)→yield 8 (a=8,b=13)→yield 13. The while True makes this generator infinite — it never stops on its own. But yield suspends execution until next() is called, so memory usage is O(1) regardless of how many values are produced. Finally, this is the key advantage of generators over lists for infinite or very large sequences.
Question 59 · Function Composition Chain · hard
Consider the following scenario and evaluate: Given: 'def compose(*funcs): def chain(x): for f in reversed(funcs): x = f(x); return x; return chain'. If 'transform = compose(str, abs, int)' and we call 'transform(-3.7)', what is returned, and trace the execution through the function composition chain?
Returns -3.7 as a string because compose applies functions left-to-right and str() is first, converting the input to a string before abs() or int() can process it
Returns 3 as an integer because compose only executes the last function in the chain (int), ignoring the preceding functions str and abs entirely
Returns -3 because int(-3.7) truncates to -3, abs() cannot process negative integers, and str() converts -3 back to an integer through implicit type casting
Returns '3' because the reversed chain executes right-to-left: int(-3.7)=-3, then abs(-3)=3, then str(3)='3', demonstrating mathematical function composition where (f∘g∘h)(x) = f(g(h(x))) applies the rightmost function first
Answer: D. Returns '3' because the reversed chain executes right-to-left: int(-3.7)=-3, then abs(-3)=3, then str(3)='3', demonstrating mathematical function composition where (f∘g∘h)(x) = f(g(h(x))) applies the rightmost function first
ExplanationReturns '3' because The compose function applies functions in reverse order (right-to-left), matching mathematical composition notation f∘g∘h(x) = f(g(h(x))). Step 1: int(-3.7) = -3 (truncation toward zero). Step 2: abs(-3) = 3 (absolute value). Step 3: str(3) = '3' (string conversion). The reversed() call is key — without it, str would execute first on -3.7, giving '-3.7', and then int('-3.7') would fail. This composition pattern is fundamental in functional programming: small, focused functions combined into pipelines. It's used in data processing, middleware stacks, and mathematical transformations.
Question 60 · Merge Two Sorted Arrays · hard
Given: 'def merge_sorted(a, b): result = []; i = j = 0; while i < len(a) and j < len(b): (result.append(a[i]), i:=i+1) if a[i] <= b[j] else (result.append(b[j]), j:=j+1); result.extend(a[i:]); result.extend(b[j:]); return result'. What does 'merge_sorted([1,3,5], [2,4,6])' return, and evaluate the time complexity of merging two sorted arrays?
Returns [1,2,3,4,5,6] but with O(n²) time complexity because each comparison requires scanning the entire remaining portion of both arrays
Returns [2,4,6,1,3,5] because the merge function processes the second array first due to the conditional expression evaluating the else branch before the if branch
Returns [1,3,5,2,4,6] because merge simply concatenates both arrays without interleaving, ignoring the comparison logic in the while loop entirely
Returns [1, 2, 3, 4, 5, 6] in O(n+m) time because the two-pointer technique compares current elements of both arrays, takes the smaller one, advances that pointer, and appends remaining elements at the end — each element is processed exactly once
Answer: D. Returns [1, 2, 3, 4, 5, 6] in O(n+m) time because the two-pointer technique compares current elements of both arrays, takes the smaller one, advances that pointer, and appends remaining elements at the end — each element is processed exactly once
ExplanationReturns [1,2,3,4,5,6] in O(n+m) time because The two-pointer merge: compare a[i] and b[j], take the smaller one. i=0,j=0: 1<=2→take 1, i=1. i=1,j=0: 3>2→take 2, j=1. i=1,j=1: 3<=4→take 3, i=2. i=2,j=1: 5>4→take 4, j=2. i=2,j=2: 5<=6→take 5, i=3. i=3 exceeds len(a), exit loop. extend(b[2:])→append 6. Result: [1,2,3,4,5,6]. Each element is visited exactly once, giving O(n+m). This is the merge step of Merge Sort, and it's why Merge Sort is O(n log n) — log n levels of recursion, each doing O(n) merging.