AI Computer Institute
Expert-curated CS & AI curriculum aligned to CBSE standards. A bharath.ai initiative. About Us

Grade 8 AI & Computer Science Practice Questions — Set 11

21 questions from the Grade 8 bank, each with its answer and a full explanation. Set 11 of 11 · 221 questions in this grade.

Reading is revision; testing is practice. Take the same questions as a timed quiz →

Question 201 · Generators · hard

Consider the following Python generator function and the code that calls it: ```python def counter(n): for i in range(1, n + 1): yield i * i g = counter(5) first = next(g) second = next(g) total = sum(g) print(first, second, total) ``` What values do `first`, `second`, and `total` hold, written as (first, second, total), once this code finishes running?

  1. (1, 4, 55)
  2. (0, 1, 29)
  3. (1, 4, 50)
  4. (1, 2, 12)

Answer: C. (1, 4, 50)

ExplanationA generator function runs none of its code when it is first called — counter(5) only creates a paused generator object. Each next() call resumes execution from exactly the point after the previous yield, not from the top of the function. The first next(g) runs the loop up to i = 1 and yields 1 * 1, so first = 1. The second next(g) resumes right after that yield, advances the loop to i = 2, and yields 2 * 2, so second = 4. The key idea being tested is that sum(g) does not restart the generator from i = 1: it keeps pulling from the same paused state, running i = 3, 4, and 5 to yield 9, 16, and 25, which add up to 50, giving (1, 4, 50). The sequence (1, 4, 55) would only occur if sum() re-ran the whole generator from scratch instead of continuing where next() left off. The sequence (0, 1, 29) assumes the loop counts from i = 0, but range(1, n + 1) always starts at 1. The sequence (1, 2, 12) drops the squaring inside the yield statement and treats each yielded value as i instead of i * i.

Question 202 · Mutable Default Arguments · medium

A function is defined as def add_item(item, cart=[]): cart.append(item); return cart. It is called three separate times, as independent statements, each as add_item('apple'), then add_item('banana'), then add_item('cherry'), with no cart argument ever passed. What does the third call return?

  1. ['cherry'] — each call starts with a fresh empty list since cart=[] is the default
  2. ['apple', 'banana', 'cherry'] — the same list object is reused and appended to across all three calls, because a mutable default argument is created ONCE when the function is defined, not on each call
  3. An error, because a list cannot be used as a default argument in Python
  4. ['apple', 'banana', 'cherry'] but in a new list each time with the same values coincidentally

Answer: B. ['apple', 'banana', 'cherry'] — the same list object is reused and appended to across all three calls, because a mutable default argument is created ONCE when the function is defined, not on each call

ExplanationPython evaluates default argument values exactly once, when the function is defined — not once per call. Since cart=[] creates one list object at definition time, every call that omits the cart argument shares that SAME list. The first call appends 'apple' to it, the second appends 'banana' to that already-modified list, and the third appends 'cherry' — so by the third call the shared list already contains all three items. This is one of Python's most notorious gotchas; the fix is def add_item(item, cart=None): if cart is None: cart = [].

Question 203 · Shallow vs Deep Copy · medium

list_a = [1, 2, [3, 4]]; list_b = list_a.copy(); list_b[2].append(5). After this, what is list_a?

  1. [1, 2, [3, 4]] — list_b is an independent copy, so modifying it never affects list_a
  2. [1, 2, [3, 4, 5]] — .copy() makes a SHALLOW copy: the outer list is new, but the nested list [3, 4] is still the same shared object referenced by both list_a and list_b
  3. [1, 2, [3, 4], 5] — the 5 gets appended to list_a's top level
  4. An error, because .append() cannot be called on a nested list accessed through a copy

Answer: B. [1, 2, [3, 4, 5]] — .copy() makes a SHALLOW copy: the outer list is new, but the nested list [3, 4] is still the same shared object referenced by both list_a and list_b

Explanationlist.copy() (like list[:] or list(x)) performs a SHALLOW copy: it creates a new outer list, but every element inside is copied by REFERENCE, not by value. For immutable elements like 1 and 2 this distinction is invisible, but list_a[2] and list_b[2] both point to the exact same inner list object [3, 4]. Appending 5 through list_b[2] mutates that shared object, so the change is visible through list_a too. To get full independence for nested structures, you need copy.deepcopy(list_a), which recursively copies every nested mutable object.

Question 204 · f-string Format Specifiers · easy

price = 1234.5; print(f'Price: ₹{price:,.2f}'). What gets printed when this is computed?

  1. Price: ₹1234.5
  2. Price: ₹1,234.50 — the comma adds thousands separators and .2f rounds/pads to exactly 2 decimal places
  3. Price: ₹1,234.5
  4. An error, because f-strings cannot combine a comma and a decimal specifier

Answer: B. Price: ₹1,234.50 — the comma adds thousands separators and .2f rounds/pads to exactly 2 decimal places

ExplanationInside an f-string's format spec (after the colon), ',' inserts a thousands separator and '.2f' formats as fixed-point with exactly 2 digits after the decimal, padding with a zero if needed. This happens because the comma groups '1' and '234' for the thousands separator, and .2f turns .5 into .50 by padding to exactly two decimal digits. These two specifiers combine freely — the general form is {value:,.Nf} for currency-style formatting, which is exactly why it's the standard way to print prices in Python.

Question 205 · Set Operations · easy

a = {1, 2, 3, 4}; b = {3, 4, 5, 6}. What does a ^ b (the symmetric difference operator) compute and return?

  1. {3, 4} — the elements common to both sets
  2. {1, 2, 3, 4, 5, 6} — all elements from both sets
  3. {1, 2, 5, 6} — the elements that are in exactly ONE of the two sets, but not both
  4. {1, 2} — the elements only in a

Answer: C. {1, 2, 5, 6} — the elements that are in exactly ONE of the two sets, but not both

ExplanationThe ^ operator on sets computes the SYMMETRIC difference: everything that appears in a or b but NOT in both. Working it out: a has {1,2,3,4}, b has {3,4,5,6}. The elements 3 and 4 appear in BOTH sets, so they are excluded. What remains is 1 and 2 (only in a) plus 5 and 6 (only in b), giving {1, 2, 5, 6}, because those are exactly the elements NOT shared by both sets. This differs from a | b (union, all 6 elements), a & b (intersection, just {3,4}), and a - b (elements only in a, just {1,2}).

Question 206 · Binary Search Complexity · medium

A binary search is implemented on a sorted list of 10 elements (indices 0-9) to find the value 7, which is not present in the list. The search compares against indices in this order: mid=4, then mid=7, then mid=8. What is the maximum number of comparisons a correctly-implemented binary search needs on a list of 10 elements before concluding the value is absent?

  1. 10, since in the worst case it must check every element like a linear search
  2. log2(10) rounded UP, which is 4 — each comparison halves the remaining search space, and ceil(log2(10)) = 4 covers even the worst case
  3. 5, exactly half of 10
  4. 1, because binary search always finds the answer in one step by definition

Answer: B. log2(10) rounded UP, which is 4 — each comparison halves the remaining search space, and ceil(log2(10)) = 4 covers even the worst case

ExplanationBinary search's defining property is that each comparison eliminates half of the remaining candidates, giving O(log n) worst-case comparisons. For n=10, the number of times you can halve 10 before reaching a single element (or empty range) is ceil(log2(10)) = ceil(3.32) = 4. Concretely: 10 candidates -> ~5 -> ~2-3 -> ~1 -> 0 (empty, search space exhausted), which is 4 comparisons in the worst case. This logarithmic scaling is exactly why binary search on a sorted list of a million items needs only about 20 comparisons, not a million.

Question 207 · Sort Stability · medium

You need to sort a list of student records by grade, and among students with the SAME grade, you need the original relative order (e.g., alphabetical by name, which the list was already in) to be preserved. Which property must your sorting algorithm have?

  1. It must be the fastest possible algorithm, regardless of any other property
  2. It must be a STABLE sort — one that never reorders two elements that compare as equal, so ties keep their original relative order
  3. It must sort in-place, without using any extra memory
  4. It must be a recursive algorithm rather than an iterative one

Answer: B. It must be a STABLE sort — one that never reorders two elements that compare as equal, so ties keep their original relative order

ExplanationA sorting algorithm is called STABLE if it never swaps the relative order of two elements that are considered equal by the comparison key — here, two students sharing the same grade. Python's built-in sorted() and list.sort() are guaranteed stable (they use Timsort), which is exactly why 'sort by grade, keeping alphabetical order for ties' works correctly if the list was already alphabetical: you simply sort by grade once, and stability preserves the earlier alphabetical order within each grade group. An UNSTABLE sort (like a naive quicksort) offers no such guarantee — ties could end up in any order.

Question 208 · Exception Chaining · medium

def risky(): try: return 1 / 0 except ZeroDivisionError as e: raise ValueError('bad input') from e. If this function is called and the exception propagates uncaught, what does Python's traceback show?

  1. Only the ValueError, with no mention of the original ZeroDivisionError
  2. Only the original ZeroDivisionError, since 'from e' suppresses the new exception
  3. BOTH exceptions, chained: the ZeroDivisionError is shown as 'the direct cause' of the ValueError, giving a full picture of the original failure and how it was translated
  4. A SyntaxError, because 'raise ... from ...' is not valid Python syntax

Answer: C. BOTH exceptions, chained: the ZeroDivisionError is shown as 'the direct cause' of the ValueError, giving a full picture of the original failure and how it was translated

ExplanationThe 'raise NewException(...) from original_exception' syntax explicitly chains exceptions: it sets the new exception's __cause__ attribute to the original one. Python's traceback then prints BOTH: it shows the original ZeroDivisionError first, labeled 'The above exception was the direct cause of the following exception', followed by the new ValueError. This is deliberately different from a bare 'except: raise ValueError(...)' (which Python still chains automatically via __context__, but labels differently as 'During handling of the above exception, another exception occurred') — using 'from e' makes the causal relationship explicit and intentional rather than incidental.

Question 209 · Iterator Protocol · hard

class Counter: def __iter__(self): self.n = 0; return self. def __next__(self): if self.n < 3: self.n += 1; return self.n; else: raise StopIteration. Given c = Counter(), what does list(c) produce?

  1. [0, 1, 2] — counting starts from 0
  2. [1, 2, 3] — __iter__ resets n to 0, then __next__ increments BEFORE returning, checking n<3 each time, so it yields 1, then 2, then 3, then stops
  3. An infinite loop, since there is no upper bound defined anywhere in the class
  4. A TypeError, because Counter does not define __len__

Answer: B. [1, 2, 3] — __iter__ resets n to 0, then __next__ increments BEFORE returning, checking n<3 each time, so it yields 1, then 2, then 3, then stops

Explanationlist() on any object calls iter(c) first, which invokes __iter__ — here that resets self.n to 0 and returns self (meaning Counter is its OWN iterator). Then list() repeatedly calls __next__: first call has n=0 which is <3, so n becomes 1 and 1 is returned; second call has n=1<3, so n becomes 2 and 2 is returned; third call has n=2<3, so n becomes 3 and 3 is returned; fourth call has n=3, which is NOT <3, so StopIteration is raised and list() stops collecting. The result is [1, 2, 3]. This is the exact protocol (__iter__ returning an object with __next__, which eventually raises StopIteration) that powers every for-loop in Python under the hood.

Question 210 · Hash Collision Resolution · medium

A hash table with 8 buckets stores keys by computing hash(key) % 8. Keys 'cat' and 'dog' both hash to bucket 3 — a COLLISION. Using separate chaining (the most common collision-resolution strategy taught alongside hash tables), what actually happens when 'dog' is inserted after 'cat' is already at bucket 3?

  1. 'dog' overwrites 'cat', permanently losing the earlier entry
  2. Python raises an error and refuses to insert 'dog'
  3. Bucket 3 stores BOTH entries, typically as a small linked list (or similar structure) of key-value pairs; a lookup for a key then scans that bucket's list, comparing keys until it finds a match
  4. 'dog' is silently placed at a random different bucket instead

Answer: C. Bucket 3 stores BOTH entries, typically as a small linked list (or similar structure) of key-value pairs; a lookup for a key then scans that bucket's list, comparing keys until it finds a match

ExplanationSeparate chaining handles collisions by letting each bucket hold a small COLLECTION (commonly a linked list) of entries rather than just one. When 'dog' also hashes to bucket 3, it is appended to that bucket's chain alongside 'cat' rather than overwriting it or erroring. Looking up a key then means: compute its bucket, then walk that bucket's chain comparing each stored key until a match is found (or the chain ends, meaning the key is absent). This is why hash table performance degrades if TOO many keys collide into the same bucket — the chain gets long and lookup degrades from O(1) average toward O(n) worst case, which is exactly why a good hash function spreading keys evenly matters.

Question 211 · Deque as Queue · easy

from collections import deque; q = deque(); q.append(1); q.append(2); q.appendleft(0); print(q.popleft()). What is printed, and what does the deque contain afterward?

  1. Prints 2 (last appended); deque now contains [0, 1]
  2. Prints 0 (leftmost element); deque now contains [1, 2] — append() adds to the right, appendleft() adds to the left, and popleft() removes and returns from the left, giving deque exactly the FIFO queue behavior its name promises
  3. Prints 1; deque now contains [0, 2]
  4. An error, because deque does not support popleft() unless imported separately

Answer: B. Prints 0 (leftmost element); deque now contains [1, 2] — append() adds to the right, appendleft() adds to the left, and popleft() removes and returns from the left, giving deque exactly the FIFO queue behavior its name promises

ExplanationTrace the operations step by step: q.append(1) makes the deque [1]. q.append(2) adds to the right, making it [1, 2]. q.appendleft(0) adds to the LEFT, making it [0, 1, 2]. q.popleft() removes and returns the leftmost element, which is 0, leaving the deque as [1, 2]. This left-add/right-add/left-remove combination is exactly why deque (double-ended queue) is the standard Python structure for implementing an efficient FIFO queue — unlike list.pop(0), which is O(n) because it has to shift every remaining element, deque's popleft() is O(1).

Question 212 · Recursion Depth Limits · medium

def factorial(n): return 1 if n == 0 else n * factorial(n - 1). If Python's default recursion limit is 1000 and you call factorial(1500), what happens?

  1. It returns the correct (very large) factorial value, since Python integers have unlimited precision
  2. It raises a RecursionError, because each recursive call adds a new frame to the call stack, and 1500 nested calls exceeds Python's default maximum recursion depth of 1000
  3. It silently returns 0 once the numbers get too large
  4. It runs correctly but takes an unusually long time due to the size of the result

Answer: B. It raises a RecursionError, because each recursive call adds a new frame to the call stack, and 1500 nested calls exceeds Python's default maximum recursion depth of 1000

ExplanationEven though Python's integers can grow arbitrarily large (so the MATH of factorial(1500) is not the problem), each recursive call to factorial() creates a new stack frame that stays on the call stack until that call returns. factorial(1500) needs roughly 1500 nested, not-yet-returned calls before the base case n==0 is reached and the unwinding begins — but Python's interpreter enforces a default limit of 1000 stack frames (sys.getrecursionlimit()) specifically to prevent a runaway recursive call from crashing the underlying C stack. Exceeding it raises RecursionError: maximum recursion depth exceeded, well before the actual arithmetic ever becomes a problem. An iterative loop, or sys.setrecursionlimit() with caution, would sidestep this.

Question 213 · JSON Serialization · easy

import json; data = {'name': 'Aisha', 'age': 16, 'city': None}; print(json.dumps(data)). What does this print, and specifically what does Python's None become?

  1. {'name': 'Aisha', 'age': 16, 'city': None} — identical Python syntax, just as a string
  2. {"name": "Aisha", "age": 16, "city": null} — JSON requires double quotes for strings/keys, and Python's None maps to JSON's null (there is no 'None' keyword in JSON)
  3. An error, because None cannot be serialized to JSON
  4. {"name": "Aisha", "age": 16, "city": "None"} — None becomes the string "None"

Answer: B. {"name": "Aisha", "age": 16, "city": null} — JSON requires double quotes for strings/keys, and Python's None maps to JSON's null (there is no 'None' keyword in JSON)

Explanationjson.dumps() converts a Python object into a JSON-formatted string, translating Python types to their JSON equivalents: dict keys and string values MUST use double quotes in valid JSON (Python's single quotes are not valid JSON syntax), and Python's None specifically maps to JSON's null keyword (JSON has no concept of 'None' — null is its only representation of an absent value). So the dict becomes {"name": "Aisha", "age": 16, "city": null}, with True/False similarly becoming JSON's lowercase true/false. This exact mapping is why JSON is a reliable interchange format between Python and virtually any other language.

Question 214 · Pandas Boolean Filtering · medium

import pandas as pd; df = pd.DataFrame({'name': ['Riya', 'Kabir', 'Meera'], 'score': [85, 62, 91]}); result = df[df['score'] > 70]['name']. What does result contain?

  1. ['Riya', 'Kabir', 'Meera'] — all names, since the filter only affects the score column
  2. A Series containing 'Riya' and 'Meera' — the boolean mask df['score'] > 70 selects only the rows where score exceeds 70 (85 and 91, not 62), and then ['name'] extracts just the name column from those filtered rows
  3. A single value, 91, the maximum score
  4. An error, because you cannot chain two square-bracket selections in pandas

Answer: B. A Series containing 'Riya' and 'Meera' — the boolean mask df['score'] > 70 selects only the rows where score exceeds 70 (85 and 91, not 62), and then ['name'] extracts just the name column from those filtered rows

Explanationdf['score'] > 70 evaluates elementwise, producing a boolean Series [True, False, True] (85>70, 62 is not >70, 91>70). Using that boolean Series to index df (df[boolean_series]) keeps only the rows where the mask is True — Riya's row and Meera's row, dropping Kabir's. Then ['name'] on that filtered DataFrame selects just the name column from the remaining rows, giving a Series with 'Riya' and 'Meera'. This filter-then-select chaining (df[condition][column]) is one of the most common patterns in real pandas data-analysis code.

Question 215 · CSV Header Handling · easy

Given the CSV file contents: name,score\nAisha,85\nRohan,72 — what does the FIRST row ("name,score") represent when this file is read with Python's csv.DictReader?

  1. It is treated as a normal data row, giving a dict {'name': 'Aisha', 'score': '85'} on the first iteration
  2. It is automatically consumed as the HEADER row, and DictReader uses its values ('name', 'score') as the KEYS for every subsequent row's dictionary — so the first iteration yields {'name': 'Aisha', 'score': '85'}, using 'name'/'score' as keys, not as data
  3. It is skipped entirely and never used for anything
  4. It causes an error because DictReader requires all values to be quoted

Answer: B. It is automatically consumed as the HEADER row, and DictReader uses its values ('name', 'score') as the KEYS for every subsequent row's dictionary — so the first iteration yields {'name': 'Aisha', 'score': '85'}, using 'name'/'score' as keys, not as data

Explanationcsv.DictReader's defining behavior is to automatically treat the FIRST row of the file as the fieldnames (unless you explicitly pass a different fieldnames argument), rather than yielding it as data. So for the file name,score / Aisha,85 / Rohan,72, DictReader reads 'name,score' once at initialization to learn the field names, then produces one dict per REMAINING row: {'name': 'Aisha', 'score': '85'}, then {'name': 'Rohan', 'score': '72'}. Note both values come back as strings ('85', not 85) — CSV has no type information, so converting 'score' to an int is something your code must do explicitly, e.g. int(row['score']).

Question 216 · Boolean Short-Circuit Evaluation · medium

What does 0 and 'hello' evaluate to in Python, and what general rule does this demonstrate?

  1. True, because 'hello' is a non-empty string
  2. 0, because Python's 'and' returns the FIRST operand if it is falsy, short-circuiting WITHOUT even evaluating the second operand — it never needs to check 'hello' since 0 already determines the result is falsy
  3. False, because 0 is falsy so the whole expression becomes the boolean False
  4. An error, because 'and' cannot compare an integer and a string

Answer: B. 0, because Python's 'and' returns the FIRST operand if it is falsy, short-circuiting WITHOUT even evaluating the second operand — it never needs to check 'hello' since 0 already determines the result is falsy

ExplanationPython's 'and'/'or' operators do NOT return True/False the way many languages do — they return one of the actual OPERANDS, chosen by short-circuit logic. For 'and': if the first operand is falsy (0, '', None, False, empty containers), Python immediately returns that first operand WITHOUT evaluating the second one at all, since the overall result must be falsy regardless. Here 0 is falsy, so 0 and 'hello' returns 0 directly — 'hello' is never even looked at. This matters practically: it's why patterns like config.get('key') or 'default' work (returns the dict value if truthy, otherwise falls through to 'default'), and why an expression like x and x.attribute safely avoids an AttributeError when x is None.

Question 217 · Equality vs Identity (== vs is) · hard

a = 300; b = 300; print(a == b); print(a is b). What is printed for each, and why might they differ?

  1. Both print True, because Python always considers equal integers to be the same object
  2. == prints True (values are equal) but 'is' commonly prints False for large integers like 300 — CPython caches and reuses small integers (typically -5 to 256) as shared objects, but 300 falls outside that range, so a and b are typically separate objects with equal VALUE but different IDENTITY
  3. Both print False, because 300 is too large to compare directly
  4. 'is' prints True and == prints False, the reverse of the typical case

Answer: B. == prints True (values are equal) but 'is' commonly prints False for large integers like 300 — CPython caches and reuses small integers (typically -5 to 256) as shared objects, but 300 falls outside that range, so a and b are typically separate objects with equal VALUE but different IDENTITY

Explanation== checks VALUE equality (do these two objects represent the same value?), while 'is' checks IDENTITY (are these literally the same object in memory, i.e., same id())? For 300 == 300, both sides have the value 300, so == is True regardless of implementation details. But CPython (the standard Python implementation) has a small-integer cache that pre-creates and reuses single shared objects for integers roughly -5 through 256, purely as a memory/speed optimization — numbers outside that range, like 300, are typically allocated as fresh, separate objects each time. So a is b is often False for 300 (implementation-dependent, not guaranteed by the language spec), even though a == b is always True. The practical lesson: always use == for value comparison; reserve 'is' specifically for identity checks like 'is None'.

Question 218 · Class Variables vs Instance Variables · hard

class BankAccount: interest_rate = 0.02 (a CLASS variable, shared by all instances). def __init__(self, balance): self.balance = balance (an INSTANCE variable, unique per object). If acc1 = BankAccount(1000) and acc2 = BankAccount(2000), and then you run acc1.interest_rate = 0.05, what happens to acc2.interest_rate?

  1. acc2.interest_rate also becomes 0.05, since interest_rate is shared across all instances
  2. acc2.interest_rate stays 0.02 — the assignment acc1.interest_rate = 0.05 does NOT modify the shared class variable; it creates a NEW instance variable on acc1 specifically that shadows (hides) the class variable for acc1 only, leaving the class variable and every other instance (like acc2) untouched
  3. Both acc1 and acc2 raise an AttributeError afterward
  4. The BankAccount class itself is deleted

Answer: B. acc2.interest_rate stays 0.02 — the assignment acc1.interest_rate = 0.05 does NOT modify the shared class variable; it creates a NEW instance variable on acc1 specifically that shadows (hides) the class variable for acc1 only, leaving the class variable and every other instance (like acc2) untouched

ExplanationReading interest_rate on an instance (like acc1.interest_rate before any assignment) falls back to the shared class-level attribute if the instance has no attribute of its own name. But ASSIGNING to acc1.interest_rate = 0.05 does something different: it creates a brand-new instance attribute named interest_rate directly on acc1, which then shadows (takes priority over) the class attribute whenever accessed through acc1 specifically. The class attribute BankAccount.interest_rate itself is completely unchanged, and acc2 — having no instance attribute of its own — still falls back to reading the original class-level 0.02. To actually change the shared value for every instance, you would need to assign to BankAccount.interest_rate = 0.05 (on the class itself), not on one instance.

Question 219 · Tuple/Sequence Unpacking · easy

scores = [91, 85, 78]; top, mid, low = scores. What are the values of top, mid, and low after this line runs, and what would happen if scores had 4 elements instead of exactly 3?

  1. top=91, mid=85, low=78 — Python UNPACKS the list by position into the three variables in order; if scores had 4 elements instead, this line would raise a ValueError (too many values to unpack) since the variable count must match exactly
  2. top=78, mid=85, low=91 — unpacking assigns from right to left
  3. top, mid, and low all become the full list [91, 85, 78]
  4. This is invalid syntax; multiple assignment requires the tuple() function explicitly

Answer: A. top=91, mid=85, low=78 — Python UNPACKS the list by position into the three variables in order; if scores had 4 elements instead, this line would raise a ValueError (too many values to unpack) since the variable count must match exactly

ExplanationPython's multiple-assignment unpacking takes any sequence (list, tuple, etc.) on the right and assigns its elements POSITIONALLY, left to right, to the names on the left: top gets scores[0]=91, mid gets scores[1]=85, low gets scores[2]=78. This requires an EXACT count match — if scores had 4 elements, Python would raise ValueError: too many values to unpack (expected 3), since it has no rule for silently dropping or combining extra values (unless you explicitly use a starred name like *rest to absorb the leftovers, e.g. top, mid, *rest = scores).

Question 220 · String Slicing · medium

word = 'Bengaluru'; print(word[-3:]); print(word[::2]). What do these two print statements output?

  1. 'uru' then 'Bnauu' — the first slice takes the last 3 characters (negative indices count backward from the end), and the second takes every 2nd character starting from index 0 (step=2) across the whole 9-character string, landing on indices 0,2,4,6,8
  2. 'ruu' then 'engl' — negative-index slicing reverses character order by default
  3. 'ngal' then 'Bengaluru' — a step of 2 is ignored when no start/stop is given, so the full string prints unchanged
  4. 'uru' then 'enaou' — the step slice starts counting from index 1, not index 0

Answer: A. 'uru' then 'Bnauu' — the first slice takes the last 3 characters (negative indices count backward from the end), and the second takes every 2nd character starting from index 0 (step=2) across the whole 9-character string, landing on indices 0,2,4,6,8

Explanationword[-3:] means 'start 3 characters from the end, go through the end': for 'Bengaluru' (9 characters, indices 0-8: B-e-n-g-a-l-u-r-u), index -3 lands on 'u' at position 6, so the slice covers positions 6,7,8 giving 'uru'. word[::2] means 'start at the implicit beginning, go to the implicit end, step by 2' — collecting every second character: index 0='B', 2='n', 4='a', 6='u', 8='u', which concatenates to 'Bnauu' (5 characters, since a 9-character string has 5 even indices: 0,2,4,6,8). Verified directly in Python: 'Bengaluru'[-3:] == 'uru' and 'Bengaluru'[::2] == 'Bnauu'.

Question 221 · Big-O: List vs Set Membership · medium

You need to check, over and over inside a loop, whether a student ID is in a roster of 200,000 registered students. Should the roster be stored as a Python list or a set for this repeated membership-checking task, and how would you evaluate which is better?

  1. A list, because lists preserve insertion order and sets do not
  2. A set — checking 'x in collection' on a list is O(n), scanning element by element in the worst case, while a set is backed by a hash table giving O(1) average-case membership checks; for 200,000 lookups against a 200,000-item roster, this is the difference between roughly instant and painfully slow
  3. It makes no difference — Python optimizes 'in' identically for both list and set under the hood
  4. A list, because sets cannot contain string values like student IDs

Answer: B. A set — checking 'x in collection' on a list is O(n), scanning element by element in the worst case, while a set is backed by a hash table giving O(1) average-case membership checks; for 200,000 lookups against a 200,000-item roster, this is the difference between roughly instant and painfully slow

ExplanationThe 'in' operator's cost depends entirely on the underlying data structure. For a list, Python has no choice but to scan from the start until it finds a match or reaches the end — O(n) in the worst case, meaning the check gets slower as the list grows. A set is backed by a hash table (the same mechanism as a dict's keys): it computes the item's hash and jumps almost directly to where it would be, giving O(1) average-case lookup regardless of how large the set is. Measured directly: checking membership 200,000 times against a 200,000-element collection took roughly 1.5ms total for a set versus about 1.5 SECONDS for a list in the same test — a difference of roughly 1000x, which only grows as the collection gets bigger. This is precisely why 'convert the list to a set first' is one of the most common real performance fixes in Python code, because repeated membership checks are exactly the pattern where the O(n)-vs-O(1) gap matters most.
← Set 10