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 4

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

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

Question 61 · Breadth-First Search · hard

Analyze: 'def bfs(graph, start): visited = set(); queue = [start]; visited.add(start); order = []; while queue: node = queue.pop(0); order.append(node); [queue.append(n) and visited.add(n) for n in graph.get(node, []) if n not in visited]; return order'. Given 'graph = {1:[2,3], 2:[4], 3:[4,5], 4:[], 5:[]}', what does 'bfs(graph, 1)' return, and evaluate why BFS explores nodes level-by-level?

  1. Returns [1,2,4,3,5] because BFS processes nodes in depth-first order when using a list instead of a proper queue data structure in Python
  2. Returns [1,3,5,2,4] because BFS starts from the rightmost neighbor and works leftward through the adjacency list at each level
  3. Returns [1,2,3,4,5] but the order could randomly be [1,3,2,5,4] because BFS does not guarantee any specific ordering among nodes at the same depth level
  4. Returns [1, 2, 3, 4, 5] because BFS uses a FIFO queue — it processes 1 first, enqueues neighbors [2,3], then processes 2 (enqueues 4), processes 3 (enqueues 5, skips 4 as visited), processes 4, processes 5, exploring the graph level-by-level from the start node outward

Answer: D. Returns [1, 2, 3, 4, 5] because BFS uses a FIFO queue — it processes 1 first, enqueues neighbors [2,3], then processes 2 (enqueues 4), processes 3 (enqueues 5, skips 4 as visited), processes 4, processes 5, exploring the graph level-by-level from the start node outward

ExplanationFirst, returns [1,2,3,4,5]. BFS trace: Queue=[1], visit 1→enqueue [2,3]. Then, queue=[2,3], visit 2→enqueue [4]. Queue=[3,4], visit 3→enqueue [5] (4 already visited). Queue=[4,5], visit 4→no unvisited neighbors. Queue=[5], visit 5→empty. Order: [1,2,3,4,5]. BFS explores level-by-level because FIFO ordering ensures all nodes at distance d are processed before any node at distance d+1. This guarantees shortest paths in unweighted graphs. The visited set prevents revisiting nodes (crucial for cycles). Finally, queue.pop(0) is O(n) — in production, use collections.deque for O(1) popleft().

Question 62 · List Comprehensions · hard

Consider the Python code: `x = [1, 2, 3, 4, 5]; result = [i*3 for i in x if i % 2 == 0]`. Evaluate what happens when this list comprehension executes: what is the value of `result`, and why does this single-pass approach avoid the overhead of writing separate filter and transform loops?

  1. The result is [3, 6, 9, 12, 15] because the condition i % 2 == 0 is checked only after the multiplication i*3 has already been applied to every element, so the filter has no effect on which values reach the final list
  2. The result is [2, 4] because the comprehension keeps only the even values from x and never applies the i*3 expression, since the filter clause after 'for' overrides any transformation written before it
  3. The result is [6, 12] because the comprehension first tests each element with i % 2 == 0 and keeps only the even ones (2 and 4), then applies i*3 to each surviving element to get 6 and 12, doing the filtering and the transforming in a single pass over x instead of two separate loops
  4. The result is an error because a list comprehension cannot place a transformation expression like i*3 before an 'if' filter condition without first importing the itertools module

Answer: C. The result is [6, 12] because the comprehension first tests each element with i % 2 == 0 and keeps only the even ones (2 and 4), then applies i*3 to each surviving element to get 6 and 12, doing the filtering and the transforming in a single pass over x instead of two separate loops

ExplanationFor each element in x = [1, 2, 3, 4, 5], Python evaluates the 'if i % 2 == 0' condition first, before the 'i*3' expression is ever applied to that element. 1 is odd (rejected), 2 is even (kept), 3 is odd (rejected), 4 is even (kept), 5 is odd (rejected) — so filtering alone leaves [2, 4]. Only then does the comprehension apply the transformation i*3 to each surviving element: 2*3 = 6 and 4*3 = 12, giving result = [6, 12]. This matters for efficiency because the comprehension does both the filtering and the transforming in one pass over x — each element is visited exactly once. Writing this with a separate filter loop followed by a separate transform loop would mean traversing the list (or an intermediate list) twice, doing extra work for no benefit on a simple case like this.

Question 63 · Nested Data Structures · hard

Given this Python snippet: `inventory = {'item': 'Pen', 'stock': [12, 45, 7, 30]}` followed by `total = inventory['stock'][1] + inventory['stock'][3]`. What value does `total` hold, and why?

  1. total holds 75, because inventory['stock'] resolves the dictionary lookup to the list [12, 45, 7, 30], and then [1] and [3] use 0-based indexing on that list to retrieve 45 (second element) and 30 (fourth element), which sum to 75
  2. total holds 19, because Python list indices inside a dictionary value start counting from 1, so inventory['stock'][1] is the first element (12) and inventory['stock'][3] is the third element (7), which sum to 19
  3. This code raises a TypeError, because once a list is stored as a dictionary value, its elements can no longer be accessed with square-bracket indexing unless the list is first converted back to a standalone variable
  4. total holds 52, because inventory['stock'][1] correctly returns the second element (45), but inventory['stock'][3] is miscounted as the element three positions after the start of 1-based counting (7), which sum to 52

Answer: A. total holds 75, because inventory['stock'] resolves the dictionary lookup to the list [12, 45, 7, 30], and then [1] and [3] use 0-based indexing on that list to retrieve 45 (second element) and 30 (fourth element), which sum to 75

Explanationinventory['stock'] first performs a dictionary lookup by key, returning the list [12, 45, 7, 30]. Only after that lookup resolves can list indexing happen: [1] retrieves the element at position 1 using 0-based counting, which is 45 (positions are 0:12, 1:45, 2:7, 3:30), and [3] retrieves the element at position 3, which is 30. So total = 45 + 30 = 75. This is why nested structures require tracking two separate access rules in sequence — dictionary access is by key and always resolves first, while the list it returns is then indexed starting at 0, not 1. A dictionary value that happens to be a list is still a fully ordinary list — it supports indexing, slicing, and mutation exactly as any standalone list would, with no conversion needed.

Question 64 · String Manipulation · hard

Evaluate this code: 'def process(items): return [item * 2 for item in items if len(item) > 2]'. If you call process(['hi', 'hello', 'world']), what is the result and demonstrate how this combines comprehension with string length checking?

  1. An error occurs because you cannot multiply strings directly, making the function definition invalid regardless of comprehension syntax
  2. The result is ['hello', 'hello', 'world', 'world'] because the function filters for len > 2 but returns each string twice due to the multiplication operator creating duplicates in the output list
  3. The result is ['hellohello', 'worldworld'] because the comprehension filters items with len > 2 ('hello' and 'world', skipping 'hi'), then item*2 in Python concatenates strings ('hello' + 'hello' = 'hellohello'), demonstrating string repetition with multiplication and conditional filtering combined in one pass
  4. The result is [6, 10] because the function counts the doubled string lengths after filtering, returning numeric values instead of string values

Answer: C. The result is ['hellohello', 'worldworld'] because the comprehension filters items with len > 2 ('hello' and 'world', skipping 'hi'), then item*2 in Python concatenates strings ('hello' + 'hello' = 'hellohello'), demonstrating string repetition with multiplication and conditional filtering combined in one pass

ExplanationFirst, the result is ['hellohello', 'worldworld'] because in Python, string*2 means repeat the string twice ('hello'*2 = 'hellohello'). The comprehension first filters: len('hi')=2 (excluded), len('hello')=5>2 (included), len('world')=5>2 (included). Then it applies the operation: 'hello'*2 and 'world'*2. This demonstrates why comprehensions are powerful: filtering conditions and transformations happen in one readable expression. Finally, understanding string operations (multiplication = repetition) and filtering together prepares you for complex data transformations.

Question 65 · Exception Handling · hard

Given the function: 'def get_average(scores): try: return sum(scores) / len(scores); except ZeroDivisionError: return None'. If you call get_average([]) (passing an empty list), what happens and why is this pattern important for robust programming?

  1. The function returns None because sum([]) equals 0 and len([]) equals 0, so evaluating 0 / 0 inside the try block raises ZeroDivisionError, which the except block catches before executing return None. This pattern lets the program handle an empty dataset gracefully instead of crashing, which matters because real-world code often receives edge-case inputs like empty lists that should not bring down the whole application.
  2. The program crashes with an unhandled ZeroDivisionError because sum([]) actually raises a TypeError before the division ever happens, so the except ZeroDivisionError clause is never reached and cannot protect the function.
  3. The function returns 0 because dividing 0 by 0 in Python evaluates to 0 rather than raising an exception, so the except block is defined but never actually triggered for this input.
  4. The except block is skipped entirely because the division occurs as part of a return statement, and except can only catch exceptions raised by separate standalone statements inside the try block, not by expressions evaluated within a return statement.

Answer: A. The function returns None because sum([]) equals 0 and len([]) equals 0, so evaluating 0 / 0 inside the try block raises ZeroDivisionError, which the except block catches before executing return None. This pattern lets the program handle an empty dataset gracefully instead of crashing, which matters because real-world code often receives edge-case inputs like empty lists that should not bring down the whole application.

ExplanationFirst, sum([]) is 0 and len([]) is 0 for an empty list, so the expression sum(scores) / len(scores) becomes 0 / 0. Second, evaluating 0 / 0 in Python raises ZeroDivisionError at the exact moment the return statement's expression is computed - the exception is raised inside the try block regardless of whether it happens within a return, an assignment, or any other statement, because Python evaluates the expression before the statement completes. Third, the except ZeroDivisionError clause catches this exception and executes return None, so the function's actual output is None. This pattern is important because production code frequently receives edge-case inputs (like an empty list of scores) that would otherwise crash the entire program; catching the anticipated error and returning a sensible default (None, signaling "no average exists") lets the surrounding application keep running and handle the missing value deliberately, rather than failing unexpectedly.

Question 66 · Computer Science · hard

Consider this Python code: ```python import datetime start = datetime.date(2024, 2, 29) result = start + datetime.timedelta(days=365) ``` Since 2024 is a leap year but 2025 is not, what does `result` evaluate to, and why?

  1. Result equals 2025-02-28, because timedelta advances by a fixed count of calendar days rather than by a calendar year, so the 365th day from the leap date arrives one day before where February 29 would have fallen in the non-leap year 2025.
  2. Python raises a ValueError, because 2025 is not a leap year and therefore has no February 29 for the addition to resolve to.
  3. The date rolls forward to 2025-03-01, because Python advances to the next valid calendar date whenever a day-count addition would otherwise land on a February 29 that does not exist that year.
  4. Miscounting the leap-year adjustment gives 2025-02-27, since the extra day gained in 2024 must be subtracted twice when the calculation crosses into the following non-leap year.

Answer: A. Result equals 2025-02-28, because timedelta advances by a fixed count of calendar days rather than by a calendar year, so the 365th day from the leap date arrives one day before where February 29 would have fallen in the non-leap year 2025.

ExplanationFebruary 29, 2024 is day 60 of a 366-day leap year, which leaves 306 days remaining in 2024 through December 31. That accounts for 306 of the 365 requested days, leaving 59 days still to add after December 31: 31 of those land on January 31, 2025, and the remaining 28 land on February 28, 2025. Python's timedelta arithmetic never reasons about "years" or "leap days" as concepts — it simply counts elapsed calendar days from the starting date and produces whatever real date the count reaches. It cannot raise an error or silently round forward to a later date, because February 28 is a perfectly ordinary, valid date in the non-leap year 2025; there is no invalid intermediate result to trigger such behavior. This is exactly why date math spanning a leap day needs care: "365 days later" is not the same as "one calendar year later" whenever a leap day sits inside that span, since the leap day adds one extra day to the raw count that a non-leap target year has no February 29 to absorb — the next real February 29 will not occur again until 2028.

Question 67 · Python Programming · hard

A student writes the following code and runs it: ```python import json from datetime import date data = {"name": "Asha", "joined": date(2026, 1, 15)} print(json.dumps(data)) ``` This crashes with `TypeError: Object of type date is not JSON serializable`. Which single change to the `json.dumps()` call actually fixes the crash?

  1. The fix is to call json.dumps(data, default=str) — this makes the encoder fall back to str() for any object it cannot serialize natively, turning the date into a plain string
  2. Calling json.dumps(data, default=int) looks like a fix, but int() cannot convert a date object, so a TypeError is raised again instead
  3. Setting json.dumps(data, cls=str) fails immediately, because cls expects a JSONEncoder subclass, not the built-in str type
  4. Adding json.dumps(data, sort_keys=True) changes nothing about the crash — it only reorders keys in the output and never touches unserializable values

Answer: A. The fix is to call json.dumps(data, default=str) — this makes the encoder fall back to str() for any object it cannot serialize natively, turning the date into a plain string

Explanationjson.dumps() only knows how to serialize Python's built-in JSON-compatible types — dict, list, str, int, float, bool, and None. A date object isn't one of them, so the encoder raises TypeError as soon as it reaches the "joined" field. The default parameter lets you supply a fallback function that gets called on any object the encoder doesn't recognize, and passing default=str converts that object using Python's built-in str() — so date(2026, 1, 15) becomes the string '2026-01-15' and the whole dict serializes without error. Passing default=int instead would call int() on the date object, but date has no __int__ method defined, so it raises a TypeError of its own rather than fixing anything. Passing cls=str is invalid for a different reason: the cls parameter must be a subclass of json.JSONEncoder, not the built-in str type, and the encoder fails when it tries to construct str with keyword arguments like skipkeys and ensure_ascii. Adding sort_keys=True only affects the order in which keys appear in the output string — it has no bearing on which values the encoder is able to serialize, so the crash still happens.

Question 68 · Python Programming · hard

Consider the following scenario and evaluate: When using csv.DictReader on a file with 10000 rows where fieldnames=['id', 'name', 'score'] and a row has only 2 values, how does DictReader populate the third field? What's the restval behavior?

  1. Third field = None (the default restval), so the row becomes {'id': ..., 'name': ..., 'score': None}
  2. Third field is skipped entirely, so accessing row['score'] raises a KeyError
  3. Third field = empty string '', so the row becomes {'id': ..., 'name': ..., 'score': ''}
  4. Third field value = 2 (the index of 'score' in fieldnames), so the row becomes {'id': ..., 'name': ..., 'score': 2}

Answer: A. Third field = None (the default restval), so the row becomes {'id': ..., 'name': ..., 'score': None}

Explanationcsv.DictReader fills any field missing from a short row with its restval parameter, which defaults to None. With fieldnames=['id', 'name', 'score'] and a row containing only 2 values, the reader maps the two values to 'id' and 'name' in order, and since there is no third value to consume, 'score' is set to restval. Because restval was not overridden when the DictReader was constructed, its value is None, so the row becomes {'id': ..., 'name': ..., 'score': None} — no KeyError is raised, no empty string is substituted, and no index value is used, because restval exists precisely to fill gaps when a row is shorter than fieldnames.

Question 69 · Algorithms · hard

A program must find the 3 smallest values in a list of 1,000,000 numbers using Python's heapq.nsmallest(3, data). Sorting the entire list first costs roughly n log₂ n ≈ 1,000,000 × 19.93 ≈ 20 million comparisons. heapq.nsmallest instead scans the list once while maintaining a heap that never holds more than k = 3 elements at a time. Using the O(n log k) bound with log₂ 3 ≈ 1.585, about how many comparisons does the heap-based approach need, and how does that compare to full sorting?

  1. Roughly 1.6 million comparisons (n log k with k = 3) — about 12 times fewer than full sorting, since the heap only compares against its 3 stored elements while scanning the list once.
  2. Around 20 million comparisons, identical to full sorting, since heapq.nsmallest must still inspect every element in the list before it can be certain of the three smallest.
  3. Close to 1 million comparisons, because scanning for just the 3 smallest values needs only one comparison per element, giving plain O(n) time.
  4. Nearly 60 million comparisons, since maintaining a heap of size 3 during the scan adds overhead that makes this approach slower than sorting the whole list.

Answer: A. Roughly 1.6 million comparisons (n log k with k = 3) — about 12 times fewer than full sorting, since the heap only compares against its 3 stored elements while scanning the list once.

Explanationheapq.nsmallest(3, data) builds and maintains a heap that never grows past k = 3 elements, even though it scans all n = 1,000,000 values exactly once. Each of those n scan steps costs at most log₂ k = log₂ 3 ≈ 1.585 comparisons to keep the size-3 heap ordered, giving a total of about 1,000,000 × 1.585 ≈ 1.6 million comparisons — the O(n log k) bound. Full sorting instead costs O(n log n): about 1,000,000 × log₂(1,000,000) ≈ 1,000,000 × 19.93 ≈ 20 million comparisons, because sorting must place every element into its exact final position, not just identify the smallest few. Since log₂ 3 is far smaller than log₂ 1,000,000, the heap approach needs roughly 12 times fewer comparisons than a full sort, even though it still examines every element once — inspecting each element is not the same as doing full-sort-level work on each element, and a fixed-size heap of 3 items is far cheaper to maintain than sorting a million-item list.

Question 70 · Python Programming · hard

A CBSE result-processing script uses an IntEnum to represent exam grades: ```python from enum import IntEnum class Grade(IntEnum): PASS = 1 MERIT = 2 DISTINCTION = 3 student_marks = {1: "Rahul", 2: "Priya", 3: "Aisha"} level = Grade.MERIT print(level == 2) print(level is 2) print(student_marks[level]) ``` What does this code print, and why does the dictionary lookup `student_marks[level]` succeed even though `level` is a `Grade` object and not a plain integer?

  1. A KeyError is raised after printing True and False, because `student_marks` was built with plain int keys and Grade.MERIT is a distinct type from int, so the dictionary lookup fails despite the values being numerically equal.
  2. True, then False, then Priya are printed, because dictionary lookups compare by `==`/hash equality rather than identity, and Grade.MERIT shares both its equality result and hash with the integer 2, even though `level is 2` is False since they remain distinct objects.
  3. CPython's small-integer cache makes this print True, True, then Priya, since integers from -5 to 256 are singleton objects and Grade.MERIT is silently reused as that exact cached integer object, making `is` also return True.
  4. Reversing the usual roles, this prints False then True then Priya, because IntEnum overrides `==` to require exact type matches while `is` is redefined to compare only the underlying integer values.

Answer: B. True, then False, then Priya are printed, because dictionary lookups compare by `==`/hash equality rather than identity, and Grade.MERIT shares both its equality result and hash with the integer 2, even though `level is 2` is False since they remain distinct objects.

Explanation`Grade.MERIT == 2` evaluates to True because IntEnum members inherit int's `__eq__`, so equality compares the numeric value stored on the member (2) against the plain integer 2. `Grade.MERIT is 2` evaluates to False because `Grade.MERIT` is a distinct enum-member object created once when the class body runs — identity checks compare object references, and this object is not the same object as the literal 2, even though the two are equal in value. The dictionary lookup `student_marks[level]` still returns "Priya" because Python dictionaries locate a key by matching `hash()` and then confirming `==`, never by requiring `is`-identity; IntEnum also inherits int's `__hash__`, so `hash(Grade.MERIT) == hash(2)`, and the stored key 2 is found. This distinction matters beyond dictionaries too — sorting a mixed list of IntEnum members and plain ints, or checking membership with `in`, both rely on `==`, so code would misbehave the moment someone mistakenly swaps in an `is` check expecting it to behave like `==`.

Question 71 · ThreadPoolExecutor Concurrency · hard

Given the concurrent.futures.ThreadPoolExecutor scenario: 'with ThreadPoolExecutor(max_workers=4) as executor: futures = [executor.submit(process_item, item) for item in range(1000)]'. If each task takes 0.5 seconds and creates 1000 tasks, what is the approximate execution time, and why does max_workers=4 limit parallelism despite having 16 CPU cores available?

  1. Execution time is approximately 500 seconds because process_item() is CPU-bound: although max_workers=4 caps the pool at exactly 4 worker threads by design, Python's Global Interpreter Lock (GIL) still allows only one thread to execute Python bytecode at a time, so the 4 threads cannot achieve real parallel speedup and the total time stays close to the sequential baseline of 1000 x 0.5s, regardless of the 16 available CPU cores
  2. Execution time is 0.5 seconds because modern CPUs handle 1000 tasks efficiently through automatic parallelization, making max_workers irrelevant since Python optimizes thread creation internally
  3. Execution time is 500 seconds because ThreadPoolExecutor queues all tasks before execution, creating serialization overhead that prevents any parallelism regardless of worker count
  4. Execution time is 4 seconds because each worker processes tasks in round-robin order with special scheduling that distributes load across all 16 cores automatically

Answer: A. Execution time is approximately 500 seconds because process_item() is CPU-bound: although max_workers=4 caps the pool at exactly 4 worker threads by design, Python's Global Interpreter Lock (GIL) still allows only one thread to execute Python bytecode at a time, so the 4 threads cannot achieve real parallel speedup and the total time stays close to the sequential baseline of 1000 x 0.5s, regardless of the 16 available CPU cores

ExplanationFirst, ThreadPoolExecutor with max_workers=4 creates exactly 4 worker threads by design, regardless of the 16 CPU cores available. Then, process_item() is CPU-bound, and Python's Global Interpreter Lock (GIL) allows only one thread to execute Python bytecode at a time, so the 4 threads cannot run this CPU-bound code in true parallel. As a result, submitting 1000 tasks at 0.5s each does not produce the ~125s a genuine 4x speedup would give; instead, total execution time stays close to the sequential baseline of 1000 x 0.5s = 500s, since the GIL serializes bytecode execution across the threads. Finally, the 16 CPU cores go largely unused because ThreadPoolExecutor is effective for I/O-bound work, where the GIL is released during blocking calls like network or disk I/O, not CPU-bound work; ProcessPoolExecutor, which runs separate processes each with its own GIL and interpreter, would be needed for genuine CPU parallelism here.

Question 72 · pathlib.Path.glob Pattern Matching · hard

A student is scanning a results folder that stores CSVs by month, like `/data/results/2026-01/marks.csv`. The `/data/results` folder itself holds no CSV files directly — only 12 dated subfolders, each containing its own CSVs. ```python from pathlib import Path p = Path('/data/results') matches = list(p.glob('*.csv')) ``` What will `matches` contain, and why?

  1. matches will contain every CSV file from all 12 subfolders, because glob() automatically recurses into every subdirectory the same way os.walk() does, regardless of the pattern used
  2. matches will be an empty list, because the pattern '*.csv' only matches names that sit directly inside /data/results — without an explicit '**' component, glob() never descends into subdirectories, unlike os.walk() which visits every directory in the tree by default
  3. Calling glob() will raise a FileNotFoundError, since '*.csv' does not match any file that actually exists directly inside /data/results
  4. Only the CSV files from whichever subfolder sorts first alphabetically will appear in matches, since glob() stops recursing after its first matching directory

Answer: B. matches will be an empty list, because the pattern '*.csv' only matches names that sit directly inside /data/results — without an explicit '**' component, glob() never descends into subdirectories, unlike os.walk() which visits every directory in the tree by default

ExplanationThe pattern '*.csv' is a single path-component pattern — the asterisk matches names within one level only, so glob() checks just the immediate contents of /data/results for anything ending in .csv. Since no CSV files sit directly in that folder (they're all one level down, inside the 12 dated subfolders), matches ends up as an empty list; glob() simply yields nothing, it does not raise an error when a pattern matches zero items. This behaves differently from os.walk(), which by default walks into every subdirectory of the tree it's given, producing a (dirpath, dirnames, filenames) tuple for each one it visits, with no extra symbol required. To make glob() reach into those subfolders, the pattern needs a '**' component, as in glob('**/*.csv'), where '**' matches zero or more directory levels and gives glob() the same kind of recursive descent os.walk() performs automatically.

Question 73 · struct.pack Binary Serialization · hard

Consider the following scenario and evaluate: Examine the struct module: 'fmt = '>3H2I'; data = struct.pack(fmt, 100, 200, 300, 50000, 60000); unpacked = struct.unpack(fmt, data)'. What binary representation does format '>3H2I' produce, and why is the '>' prefix critical for portability across different system architectures?

  1. '>3H2I' packs 3 unsigned shorts (2 bytes each) and 2 unsigned ints (4 bytes each) in big-endian byte order, totaling 14 bytes. The '>' prefix ensures big-endian format for cross-platform consistency, preventing byte-order confusion when data moves between Intel x86 (little-endian) and ARM systems (big-endian)
  2. The format specifies 3 integers followed by 2 floats because 'H' means 'half-float' and 'I' means 'float', producing 20 bytes with automatic precision loss
  3. '>3H2I' creates a 48-bit binary structure because Python internally normalizes all formats to 16-bit chunks, regardless of the prefix character
  4. The '>' prefix is optional and irrelevant; Python automatically detects byte order from the host system, making explicit prefix specification redundant

Answer: A. '>3H2I' packs 3 unsigned shorts (2 bytes each) and 2 unsigned ints (4 bytes each) in big-endian byte order, totaling 14 bytes. The '>' prefix ensures big-endian format for cross-platform consistency, preventing byte-order confusion when data moves between Intel x86 (little-endian) and ARM systems (big-endian)

ExplanationFirst, struct.pack('>3H2I', 100, 200, 300, 50000, 60000) produces 14 bytes: 3 unsigned shorts (3×2=6 bytes) + 2 unsigned ints (2×4=8 bytes). The '>' prefix specifies big-endian byte order (network byte order), ensuring consistent binary representation across architectures. Then, without '>', Python uses native byte order (little-endian on x86, big-endian on PowerPC), causing incompatibility when sharing binary data. For example, value 256 in little-endian is 0x0001 but 0x0100 in big-endian. Finally, therefore, explicit byte-order specification is critical for serialization, network protocols, and binary file formats.

Question 74 · hashlib.sha256 Password Security · hard

Analyze a security scenario with hashlib.sha256: 'password = 'MyP@ssw0rd123'; salt = 'secure_salt_2026'; hashed = hashlib.sha256((salt + password).encode()).hexdigest()'. Is this hashing approach secure for password storage, and why would using hashlib.pbkdf2_hmac with 600,000 iterations be significantly more secure?

  1. This approach is secure because SHA-256 produces 256-bit output, which is cryptographically unbreakable, making simple salt+password concatenation sufficient for password protection
  2. Using the salt 'secure_salt_2026' with SHA-256 already secures this password storage, since a per-database salt eliminates rainbow-table lookups and iteration count only matters for hash speed benchmarking, not attacker resistance
  3. Storing the salt 'secure_salt_2026' as plain text is the only weakness here; encrypting that salt value with a separate secret key would make plain SHA-256 fully safe for password storage regardless of iteration count
  4. This approach is insecure because SHA-256 is optimized for speed (billions of hashes per second on modern GPUs), enabling brute-force attacks to crack 'MyP@ssw0rd123' in seconds. hashlib.pbkdf2_hmac with 600,000 iterations applies SHA-256 600,000 times per guess, cutting attacker throughput by roughly the same factor and pushing the crack time from seconds to weeks or longer

Answer: D. This approach is insecure because SHA-256 is optimized for speed (billions of hashes per second on modern GPUs), enabling brute-force attacks to crack 'MyP@ssw0rd123' in seconds. hashlib.pbkdf2_hmac with 600,000 iterations applies SHA-256 600,000 times per guess, cutting attacker throughput by roughly the same factor and pushing the crack time from seconds to weeks or longer

ExplanationFirst, SHA-256 without iteration is unsuitable for password hashing because it's optimized for speed: modern GPUs achieve roughly 10⁹ (one billion) hashes per second, so an attacker can try billions of guesses against 'MyP@ssw0rd123' in seconds. Then, hashlib.pbkdf2_hmac('sha256', password, salt, 600000) iterates the hash 600,000 times per guess, cutting GPU throughput to roughly 10⁹ ÷ 600,000 ≈ 1,667 hashes per second — a ~600,000× slowdown. This turns an attack that took seconds into one that takes weeks or longer. Finally, using strong iteration counts (PBKDF2 with 600K+, bcrypt with cost 12+, or Argon2) is critical for password security; raw SHA-256 is never acceptable for storing passwords.

Question 75 · list comprehensions · hard

Evaluate this Python code: 'numbers = [1, 2, 3, 4, 5]; squared = [x**2 for x in numbers if x > 2]'. What is the resulting value of squared, and analyze how list comprehension filters elements before transformation?

  1. The result is [1, 4, 9, 16, 25] because list comprehension squares all elements in the range regardless of the if condition, treating the filter as documentation only without affecting output
  2. The result is [9, 16, 25] because the if condition first filters elements (keeping only 3, 4, 5 where x > 2), then the comprehension squares those filtered values, demonstrating conditional filtering before transformation in O(n) time
  3. The result is [3, 4, 5] because list comprehension returns the filtered elements without squaring them, misunderstanding how the for and transformation work together in comprehension syntax
  4. The result is [4, 9, 16, 25] because the condition evaluates from right to left, incorrectly ordering the filtered and squared values in the output list

Answer: B. The result is [9, 16, 25] because the if condition first filters elements (keeping only 3, 4, 5 where x > 2), then the comprehension squares those filtered values, demonstrating conditional filtering before transformation in O(n) time

ExplanationStep-by-step evaluation: (1) The comprehension iterates through numbers = [1, 2, 3, 4, 5]. (2) The trailing 'if x > 2' condition is applied first to each value, filtering out 1 and 2 and keeping only 3, 4, 5. (3) Only the elements that pass this filter are then transformed by x**2: 3**2 = 9, 4**2 = 16, 5**2 = 25. (4) Collecting these gives squared = [9, 16, 25]. This shows that in '[expression for x in iterable if condition]', the condition is evaluated before the expression is applied — an element is skipped entirely if it fails the condition, so it is never squared. Without the 'if x > 2' filter, every element would be squared instead, giving [1, 4, 9, 16, 25].

Question 76 · list comprehensions · hard

Given the code: 'words = ['hello', 'world', 'python']; result = [len(w) for w in words]'. What list is produced by this comprehension, and evaluate why this approach is more Pythonic than using a for loop with append?

  1. The result is [5, 5, 6] because the comprehension iterates through words, calling len() on each string: len('hello')=5, len('world')=5, len('python')=6, creating a new list in a single expression without side effects
  2. The result is ['hello', 'world', 'python'] because len(w) is evaluated but not included in the output, returning the original list unchanged by the comprehension operation
  3. The result is ['h', 'w', 'p'] because len() extracts only the first character of each word instead of counting total character length in the comprehension
  4. The result is 16 because the comprehension calculates total character count (5+5+6=16) and returns a single integer rather than a list of individual lengths

Answer: A. The result is [5, 5, 6] because the comprehension iterates through words, calling len() on each string: len('hello')=5, len('world')=5, len('python')=6, creating a new list in a single expression without side effects

ExplanationFirst, the result is [5, 5, 6]. The comprehension iterates: w='hello' → len('hello')=5, w='world' → len('world')=5, w='python' → len('python')=6. Then, result = [5, 5, 6]. This is more Pythonic than manual loops because (1) it is declarative, expressing intent directly rather than imperative instructions, (2) it is memory-efficient as the entire expression evaluates in one pass without intermediate variables, (3) it is concise and readable, demonstrating Pythonic style, (4) it avoids append() overhead in loops. The traditional approach result = []; for w in words: result.append(len(w)) requires 4 lines and creates side effects (the list grows during iteration), whereas comprehension achieves the same goal in 1 line with pure functional semantics. Finally, this is why comprehensions are preferred for transformations throughout professional Python code because they enable faster prototyping and fewer bugs.

Question 77 · generators · hard

Consider this generator expression: 'gen = (x**2 for x in range(5) if x % 2 == 0)'. What values are produced when you iterate through this generator, and analyze how generators differ from list comprehensions in memory usage?

  1. The generator produces [0, 4, 16] immediately, storing all values in memory like a list comprehension, making it functionally equivalent with no memory advantage
  2. The generator lazily produces 0, 4, 16 one value at a time when requested, without storing the entire sequence in memory, making it superior to list comprehensions for large datasets where memory efficiency matters
  3. The generator produces 0, 1, 2, 3, 4 without filtering, ignoring the if condition because generator expressions do not support conditional filtering like list comprehensions do
  4. The generator produces nothing because parentheses indicate an expression that requires immediate conversion to list before iteration can begin

Answer: B. The generator lazily produces 0, 4, 16 one value at a time when requested, without storing the entire sequence in memory, making it superior to list comprehensions for large datasets where memory efficiency matters

ExplanationFirst, the generator produces values lazily: (1) range(5) generates 0,1,2,3,4. (2) if x % 2 == 0 filters to even numbers: 0, 2, 4. Then, (3) x**2 transforms: 0**2=0, 2**2=4, 4**2=16. (4) Iteration yields 0, then 4, then 16 on demand without storing the sequence. Generators differ fundamentally from list comprehensions in memory behavior because list [x**2 for x in range(5) if x % 2 == 0] allocates memory for all 3 values immediately (O(n) space complexity), while generator (x**2 for x in range(5) if x % 2 == 0) computes one value per next() call, using O(1) space. For range(1000000), this difference is critical—list would consume megabytes of RAM, while generator uses only kilobytes. Finally, generators are therefore essential for processing large datasets, streaming data from files, or creating infinite sequences in professional data pipelines.

Question 78 · list comprehensions · hard

Given: 'result = [x*y for x in [1, 2, 3] for y in [10, 20]]'. Evaluate what values are produced when this comprehension executes, and explain why the order of for loops in nested comprehensions matters for controlling output sequencing?

  1. The result is [10, 20, 20, 40, 30, 60] because the first for loop iterates x through [1, 2, 3], and for each x, the second for loop iterates y through [10, 20], computing all products in this nested order
  2. The result is [10, 20, 30] because nested comprehensions sum the products rather than listing them individually, reducing the output to one value per outer loop iteration
  3. The result is [10, 40, 90] because only the first element of each inner loop is multiplied with outer elements, skipping the 20 in each pair
  4. The result is [110, 220, 330] because the comprehension concatenates all multiplied values into single numbers per outer loop iteration

Answer: A. The result is [10, 20, 20, 40, 30, 60] because the first for loop iterates x through [1, 2, 3], and for each x, the second for loop iterates y through [10, 20], computing all products in this nested order

ExplanationFirst, the result is [10, 20, 20, 40, 30, 60]. Evaluation order: (1) x=1: inner loop y=10 → 1*10=10, y=20 → 1*20=20, (2) x=2: inner loop y=10 → 2*10=20, y=20 → 2*20=40, (3) x=3: inner loop y=10 → 3*10=30, y=20 → 3*20=60. Then, the nested for loops read left-to-right as nested levels: outer for x drives the loop structure, inner for y repeats for each x value. This produces a Cartesian product: all combinations of (x,y) pairs. If loop order reversed to [x*y for y in [10, 20] for x in [1, 2, 3]], output would be [10, 20, 30, 20, 40, 60]—the same values but in different order because y=10 iterates all x values first. Loop order matters because it determines the iteration pattern: outer loop controls the major grouping, inner loop varies within each group. Finally, this is critical because Cartesian products are fundamental in combinatorics, cross-joins in databases, and permutation/combination problems.

Question 79 · list comprehensions · hard

Consider this Python code: ```python data = [3, 7, 2, 9, 4, 6] result = [x * 2 for x in data if x > 4] ``` What does the list `result` contain, and what is its sum?

  1. The filter x > 4 keeps only 7, 9, and 6 from the original order, and doubling each gives result = [14, 18, 12] with a sum of 44.
  2. Treating the condition as x >= 4 lets 4 slip through the filter too, producing result = [14, 18, 8, 12] with a sum of 52.
  3. Doubling every value first and then checking which doubled results exceed 4 produces result = [6, 14, 18, 8, 12] with a sum of 58.
  4. Only the filtering step actually runs, so result equals the kept values themselves, [7, 9, 6], with a sum of 22, since x * 2 has no effect on numeric elements.

Answer: A. The filter x > 4 keeps only 7, 9, and 6 from the original order, and doubling each gives result = [14, 18, 12] with a sum of 44.

ExplanationThe condition x > 4 is evaluated on each element of the original list data = [3, 7, 2, 9, 4, 6] before any transformation happens, filtering out 3, 2, and 4 (since 4 is not strictly greater than 4) and keeping 7, 9, and 6 in their original order. Only after an element passes the filter does the expression x * 2 run on it, doubling each survivor: 7 becomes 14, 9 becomes 18, and 6 becomes 12. So result = [14, 18, 12], and the sum 14 + 18 + 12 = 44. A common mix-up is applying the doubling before the filter, or reading > as >=, both of which change which elements survive and shift the total away from 44.

Question 80 · list comprehensions · hard

Consider the expression: 'result = {x: x**2 for x in [1, 2, 3, 4, 5]}'. What dictionary is created, and analyze how dictionary comprehensions extend comprehension patterns to key-value structures? For the given code, consider how this pattern affects execution and analyze what the result demonstrates about Python semantics?

  1. The result is {1: 1, 2: 4, 3: 9, 4: 16, 5: 25} because dictionary comprehension uses x as key and x**2 as value, mapping each number to its square in a key-value pair structure that enables O(1) lookup by number
  2. The result is {1, 2, 3, 4, 5} as a set because comprehension uses curly braces, which Python interprets as set notation instead of dictionary notation
  3. The result is {'1': '1', '2': '4', '3': '9', '4': '16', '5': '25'} because dictionary comprehension converts all values to strings for storage in the dictionary structure
  4. The result is [1, 2, 3, 4, 5] as a list because dictionary comprehension syntax is invalid, and Python falls back to list comprehension with the first element only

Answer: A. The result is {1: 1, 2: 4, 3: 9, 4: 16, 5: 25} because dictionary comprehension uses x as key and x**2 as value, mapping each number to its square in a key-value pair structure that enables O(1) lookup by number

ExplanationThe result is {1: 1, 2: 4, 3: 9, 4: 16, 5: 25}, because Dictionary comprehension syntax {x: x**2 for x in [1,2,3,4,5]} creates key-value pairs: x=1 → (1:1), x=2 → (2:4), x=3 → (3:9), x=4 → (4:16), x=5 → (5:25). Each x becomes a key, each x**2 becomes the corresponding value. Dictionary comprehensions extend list comprehensions by specifying both key and value expressions separated by a colon. This enables O(1) average-case lookup: d[3] returns 9 instantly without scanning the entire dictionary. Without dictionary comprehensions, creating this mapping would require: d={}; for x in [1,2,3,4,5]: d[x]=x**2 (4 lines, creates intermediate steps). Comprehension does it in 1 line, more readable and Pythonic. Dictionary comprehensions are critical for building lookup tables, caches, and inverse mappings—common operations in algorithm optimization. Conditional dict comprehensions like {x: x**2 for x in range(10) if x%2==0} create filtered mappings.
← Set 3Set 5 →