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 9

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

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

Question 161 · Ethics in AI: When Machines Make Unfair Decisions · hard

An Indian city's AI loan-approval system computes a score for every applicant with the rule below, then approves anyone scoring 65 or above: ``` Score = 30 + (0.4 × monthly income in ₹ thousands) + zone bonus Zone A pin code → zone bonus = +25 Zone B pin code → zone bonus = +5 Approve if Score ≥ 65 ``` Applicant P lives in a Zone A pin code and Applicant Q lives in a Zone B pin code; both earn exactly ₹50,000 a month. In this city, Zone A and Zone B pin codes have historically been segregated along community lines. What happens to each applicant's loan application, and what ethical problem does this expose about the algorithm?

  1. Because caste, religion, and community are never entered into the formula, the algorithm cannot be discriminatory by definition, so the 20-point zone gap is a neutral, purely geographic risk factor with no ethical concern.
  2. The zone bonus reflects decades of genuinely lower default rates in Zone A, so P being approved and Q being rejected is legitimate risk-based lending rather than a fairness problem, even if Zone A and Zone B happen to correlate with community.
  3. Applicant P scores 75 (30 + 20 + 25) and is approved, while Applicant Q scores 55 (30 + 20 + 5) and is rejected — despite identical income — because the zone bonus lets pin code stand in for community, producing caste-correlated outcomes without caste ever appearing as an input.
  4. Since both applicants earn the same ₹50,000 income, the income component contributes nothing to the score difference, so the 65-point threshold is decided by income alone and the zone bonus has no real effect on either applicant's approval outcome.

Answer: C. Applicant P scores 75 (30 + 20 + 25) and is approved, while Applicant Q scores 55 (30 + 20 + 5) and is rejected — despite identical income — because the zone bonus lets pin code stand in for community, producing caste-correlated outcomes without caste ever appearing as an input.

ExplanationBoth applicants earn an identical ₹50,000 a month, so each gets exactly the same income component: 0.4 × 50 = 20 points, added to the base 30 points, giving 50 points each before any zone bonus. The only thing that can pull their scores apart is the zone bonus. Applicant P, in a Zone A pin code, adds +25 to reach 30 + 20 + 25 = 75, which clears the 65-point threshold and is approved. Applicant Q, in a Zone B pin code, adds only +5 to reach 30 + 20 + 5 = 55, which falls short of 65 and is rejected. Because Zone A and Zone B pin codes are historically segregated along community lines in this city, the zone bonus effectively carries the same information as caste or community, even though no field in the formula is labeled "caste" or "community." This is called proxy discrimination: an algorithm can produce outcomes that are strongly correlated with a protected characteristic purely by relying on a variable, like a pin code, that is statistically entangled with that characteristic. It is why real fair-lending audits check disparate impact across pin codes, surnames, and school names, not just whether a protected field was literally typed into the model. The other claims fall for two classic traps. Insisting the algorithm is neutral simply because caste was never an input variable is the "fairness through unawareness" fallacy — removing a variable from the formula does not remove its influence if a correlated variable (the pin code) is still present. Defending the score gap as reflecting "genuinely" lower historical default rates in Zone A is circular: if those historical rates were themselves shaped by decades of the same segregation, using them to set today's zone bonus just launders old bias into a new decision instead of removing it. And because P and Q have identical income, the entire 20-point gap in their final scores comes entirely from the zone bonus, not from income — so treating income as the real decision-maker while the zone bonus is dismissed as having "no real effect" gets the arithmetic backwards.

Question 162 · Regular Expressions in Python · hard

An Indian fintech app writes UPI transactions to a single log string. A developer wants to pull out each transaction's raw payload with a regex capture group and runs this code: ```python import re log = 'txn="UPI/408912345678" status="SUCCESS" txn="UPI/408998765432" status="FAILED"' result = re.findall(r'txn="(.*)"', log) print(result) ``` What does this code print?

  1. Two clean values: ['UPI/408912345678', 'UPI/408998765432'] — the engine is assumed to make each `.*` stop at its nearest closing quote.
  2. Just the first value: ['UPI/408912345678'] — `re.findall` is assumed to stop scanning the log once it finds one match.
  3. One string spanning both records: ['UPI/408912345678" status="SUCCESS" txn="UPI/408998765432" status="FAILED'] — because greedy `.*` backtracks only as far as the very last `"` in the whole log.
  4. An empty list, [] — the quotation marks embedded inside the log are assumed to break the pattern so nothing matches.

Answer: C. One string spanning both records: ['UPI/408912345678" status="SUCCESS" txn="UPI/408998765432" status="FAILED'] — because greedy `.*` backtracks only as far as the very last `"` in the whole log.

ExplanationThe key fact is that `.*` is greedy: it always tries to consume as many characters as it can, and only gives characters back (backtracks) one at a time when the rest of the pattern fails to match. Trace it. The engine finds `txn="` at the very start of `log` and enters the group. Since `.*` doesn't match newlines but happily matches quote characters, it first grabs everything left in the string — all the way to the end. Then it tries to match the trailing literal `"` and fails, because there's nothing left. So it gives back one character at a time and retries. It only has to give back a single character, because the very last character of `log` is itself a `"` (the closing quote of `"FAILED"`). So the group ends up capturing everything between the first `txn="` and that final `"` — swallowing the entire middle of the string, including the second `txn="..."` and `status="..."` pairs along the way. That capture is: `UPI/408912345678" status="SUCCESS" txn="UPI/408998765432" status="FAILED` Since `re.findall` with a single group returns just the captured text (not the whole match), `result` is a one-element list containing exactly that string. After this single match consumes almost the entire log, only the trailing `"` is left unscanned — too short for a second `txn="..."` match, so no second element is ever found. The two-clean-values answer is what you'd get with a *non-greedy* quantifier, `r'txn="(.*?)"'` — the `?` after `*` makes the engine stop at the nearest `"` instead of the farthest one, which is almost always what you actually want when parsing repeated quoted fields. The "stops after one match" and "empty list" answers both misjudge how backtracking works: the pattern doesn't fail on embedded quotes (regex has no concept of "already used" quotes), and `findall` always keeps scanning for further matches in whatever text remains after each match — it's just that greedy backtracking here leaves almost nothing left to scan.

Question 163 · Data Visualization: Making Numbers Tell Stories · hard

A news report compares the CBSE Class 10 pass percentages of two schools using a bar chart: School A scored 82% and School B scored 86%. To make the difference look dramatic, the report draws the chart's vertical axis starting at 80% and ending at 90%, instead of starting at 0%. On this chart, about how many times taller does School B's bar appear compared to School A's bar?

  1. About 3 times as tall, because the truncated 80-90% axis turns each bar's small excess above 80% into most of its total visible height
  2. About 1.05 times as tall, since a bar's height always stays proportional to the true value no matter where the axis starts
  3. About 4 times as tall, because a 4-percentage-point gap in the data directly becomes a 4x difference in bar height
  4. About 1.4 times as tall, because compressing the axis into a 10-point range only mildly exaggerates a small percentage gap

Answer: A. About 3 times as tall, because the truncated 80-90% axis turns each bar's small excess above 80% into most of its total visible height

ExplanationWhen the axis starts at 80 instead of 0, a bar's visible height is proportional to (value - 80), not to the value itself. School A's bar rises 82 - 80 = 2 units above the axis start, and School B's bar rises 86 - 80 = 6 units. That makes the height ratio 6 : 2 = 3, so B's bar looks three times as tall as A's. But the real pass percentages differ by only 4 percentage points, giving an actual ratio of just 86/82 ≈ 1.05 - the schools' true performance is almost identical. By starting the y-axis at 80% instead of 0%, the chart stretches a tiny real gap into a visually huge one, which is exactly the kind of truncated-axis distortion (sometimes called an inflated "lie factor") that data journalists and CBSE data-handling lessons warn readers to check for: always look at where a bar chart's axis begins before trusting how big a difference looks.

Question 164 · Building a Professional Portfolio Website · hard

You're organizing a multi-page CBSE Class 8 portfolio project with this folder structure: ``` portfolio/ ├── index.html ├── css/ │ └── style.css ├── images/ │ └── profile.jpg └── projects/ └── webdev.html ``` Inside `projects/webdev.html`, which pair of relative paths correctly loads the stylesheet and the profile image, and keeps working whether you open the file directly in a browser or host the whole `portfolio/` folder as a GitHub Pages project site?

  1. `../css/style.css` for the stylesheet link and `../images/profile.jpg` for the image src, since webdev.html is one folder level below the portfolio root
  2. `css/style.css` for the stylesheet link and `images/profile.jpg` for the image src, treating the projects folder as if it were the root
  3. `/css/style.css` for the stylesheet link and `/images/profile.jpg` for the image src, using absolute paths from the site's domain root
  4. `./css/style.css` for the stylesheet link and `./images/profile.jpg` for the image src, treating css and images as subfolders inside projects

Answer: A. `../css/style.css` for the stylesheet link and `../images/profile.jpg` for the image src, since webdev.html is one folder level below the portfolio root

ExplanationEvery relative path in HTML is resolved against the location of the file that contains it, not against the project's top-level folder. Because webdev.html lives inside projects/, one level below portfolio/, a single ../ is needed to step back out to the portfolio root before descending into css/ or images/ — so ../css/style.css and ../images/profile.jpg are the only paths that land on the real files. Writing css/style.css or ./css/style.css (both mean "look right here") tells the browser to search inside projects/css/style.css, a folder that doesn't exist, so the page loads unstyled and the image breaks — this is one of the most common bugs students hit the moment a portfolio grows past a single page. The path starting with a leading slash is an absolute path measured from whatever the browser treats as the domain root. That happens to work if a site sits at the very top of a domain, but a GitHub Pages project site is served from a URL like username.github.io/portfolio/, so /css/style.css actually points to username.github.io/css/style.css — a location that doesn't exist there either. It also fails when the file is opened straight from a computer, since file:// URLs have no shared root across folders. Only the double-dot path resolves correctly in every one of these situations.

Question 165 · Responsive Web Design — Building for Every Screen · hard

A developer writes this mobile-first CSS for a webpage's body text: ```css body { font-size: 14px; } @media (min-width: 480px) { body { font-size: 16px; } } @media (min-width: 768px) { body { font-size: 18px; } } @media (min-width: 1024px) { body { font-size: 20px; } } ``` She tests the page on a budget classroom tablet held in landscape mode, with a viewport width of exactly 900px. Which font-size actually renders, and why?

  1. 16px, because the 480px breakpoint is the first min-width condition satisfied as the screen widens, so its declaration is the one that sticks.
  2. 18px, because at 900px both the 480px and 768px breakpoints are satisfied, and since they carry equal specificity, the later rule in the stylesheet (the 768px one) overrides the earlier matching rules.
  3. 20px, because a 900px viewport is closest to the 1024px breakpoint, so the browser rounds up and applies that rule.
  4. 14px, because 900px does not exactly equal any of the three breakpoint values, so none of the media queries take effect and the base rule remains.

Answer: B. 18px, because at 900px both the 480px and 768px breakpoints are satisfied, and since they carry equal specificity, the later rule in the stylesheet (the 768px one) overrides the earlier matching rules.

ExplanationAt a 900px viewport, the browser evaluates every media query independently rather than picking a single "closest" breakpoint. The condition min-width: 480px is true (900 ≥ 480), min-width: 768px is true (900 ≥ 768), and min-width: 1024px is false (900 < 1024) — so three rules end up matching at once: the unconditional base rule, the 480px rule, and the 768px rule. All three declarations target the same body selector, so they have identical specificity, and the cascade falls back to source order: whichever matching rule appears last in the stylesheet wins. Since the 768px query's font-size: 18px comes after both the base rule and the 480px rule in the code, it overrides them, and 18px is what actually renders — not 16px, not 20px, and not the 14px fallback. This is exactly why mobile-first stylesheets are written with ascending min-width breakpoints: each new breakpoint's rule is meant to stack on top of and override every smaller breakpoint that also matches, not replace them outright. Confusing this with "only the nearest breakpoint applies" is a common bug source in real responsive layouts, where developers are surprised a style from a much smaller breakpoint is still technically "active" underneath the one they see rendered.

Question 166 · Load Balancing: Distributing Request Traffic · hard

IRCTC's ticket-booking backend uses 3 servers, and each incoming request is assigned to a server using the rule serverID = requestID mod 3, with request IDs starting from 0. During a Tatkal booking rush, engineers add a 4th server and switch the rule to serverID = requestID mod 4. Looking at request IDs 0 through 11, how many of these 12 requests end up mapped to a different server after the switch, and what does this reveal about simple modulo-based load balancing?

  1. 9 of the 12 requests are rerouted to a new server, because modulo-based routing recalculates every request's server index whenever the divisor changes, so adding one server disrupts most existing assignments.
  2. Only 3 of the 12 requests are rerouted, since request IDs that were already balanced evenly under mod 3 stay balanced under mod 4 as well.
  3. Exactly 4 requests are rerouted — only the ones whose ID is an exact multiple of the new server count, 4, since those are the only IDs affected by the new divisor.
  4. All 12 requests are rerouted, because switching the modulus value invalidates every previous mod 3 assignment without exception, since mod 3 and mod 4 never agree on any input.

Answer: A. 9 of the 12 requests are rerouted to a new server, because modulo-based routing recalculates every request's server index whenever the divisor changes, so adding one server disrupts most existing assignments.

ExplanationTrace both formulas for request IDs 0 to 11. serverID = requestID mod 3: 0,1,2,0,1,2,0,1,2,0,1,2 serverID = requestID mod 4: 0,1,2,3,0,1,2,3,0,1,2,3 Comparing position by position: IDs 0, 1, and 2 happen to land on the same server under both rules (0→0, 1→1, 2→2), but every ID from 3 through 11 lands on a different server than before. That's 3 unchanged and 9 changed out of 12 — a 75% disruption rate from adding just one extra server. This matters because plain modulo routing ties every request's server assignment to the total server count. Change that count even slightly — scaling up during a Tatkal rush, or losing a server to a crash — and almost the entire mapping shifts, not just the requests handled by the new server. In a real system this would mean thousands of users mid-session (say, mid-payment on a booking) get silently routed to a server that has no record of their session, causing failed transactions right when traffic is heaviest. This exact weakness is why production load balancers use consistent hashing instead: it's designed so that adding or removing one server only remaps the small share of keys that specifically belong to that server, leaving the rest of the mapping untouched.

Question 167 · Building Real-Time Chat Applications · hard

Priya is building a real-time class discussion app for her CBSE Class 8 friends using HTTP polling instead of WebSockets: every client's browser sends a "check for new messages" request to the server once every 4 seconds, and each request gets an instant reply (negligible processing time) — so the *only* source of delay is how long a message has to sit on the server before the next poll happens to pick it up. If a friend's message can land on the server at any random moment within a 4-second polling window, equally likely at any instant, what is the average delay, in seconds, between the moment a message arrives on the server and the moment the recipient's app actually displays it?

  1. 2 seconds — because on average a message arrives halfway through the polling interval, so it waits half of the 4-second window before the next poll picks it up
  2. 4 seconds — because the recipient must always wait for a complete polling cycle to finish before the next check occurs, regardless of when the message actually arrived
  3. 0 seconds — because each poll request itself returns instantly once it reaches the server, so there is no meaningful waiting time left to average over
  4. 8 seconds — because the message must wait for one full poll cycle to detect it and then a second full cycle to confirm and deliver it before it can be displayed

Answer: A. 2 seconds — because on average a message arrives halfway through the polling interval, so it waits half of the 4-second window before the next poll picks it up

ExplanationWhen a message can arrive at the server at any random moment during the 4-second gap between two polls, its wait time before being picked up ranges from almost 0 seconds (it arrives just before a poll fires) up to almost the full 4 seconds (it arrives just after a poll fires). Averaged uniformly across all these equally likely arrival moments, the expected wait is exactly half the polling interval: 4 ÷ 2 = 2 seconds. This "average half-interval delay" is a fundamental cost of polling, independent of how fast the server itself responds — it is precisely why production chat systems like WhatsApp and Instagram DMs use WebSockets instead: a WebSocket keeps one connection open so the server can push a message the instant it exists, replacing this unavoidable 2-second average wait with just the raw network latency, typically well under 100 milliseconds.

Question 168 · Fake News and Misinformation Detection: Thinking Critically · hard

A school's AI-based WhatsApp misinformation checker scores every forwarded message using the rule table below, starting from a trust score of 100: ``` IF publisher NOT on verified list: score -= 30 IF message uses ALL CAPS or 3+ "!": score -= 20 IF claim contradicted by >= 2 score -= 25 independent fact-checkers: IF source domain age < 6 months: score -= 10 IF forwarded through >= 5 WhatsApp hops: score -= 15 IF score < 60: label = "Likely Misinformation" ELIF score < 80: label = "Verify Before Sharing" ELSE: label = "Likely Reliable" ``` A message reads: "BREAKING!!! Govt to deposit ₹5000 in every Aadhaar-linked account — click now!!!" It reached you after being forwarded only 3 times, comes from a website registered 2 months ago that is not on the verified-publisher list, and has already been debunked by PIB Fact Check, Alt News, and Boom — three independent fact-checkers. What trust score and label does the checker assign to this message?

  1. Trust score = 15, which falls below the 60-point threshold — the app should flag it as Likely Misinformation
  2. Trust score = 0, which falls below the 60-point threshold — the app should flag it as Likely Misinformation
  3. Trust score = 40, which falls below the 60-point threshold — the app should flag it as Likely Misinformation
  4. Trust score = 70, which falls in the 60-79 range — the app should flag it as Verify Before Sharing, not Likely Misinformation

Answer: A. Trust score = 15, which falls below the 60-point threshold — the app should flag it as Likely Misinformation

ExplanationThe message triggers four of the five red-flag rules. The publisher is not on the verified list (-30). It uses ALL CAPS and multiple exclamation marks (-20). It has been contradicted by three independent fact-checkers, which satisfies the "2 or more fact-checkers" condition (-25). And its source domain is 2 months old, under the 6-month cutoff (-10). Those four penalties total 30 + 20 + 25 + 10 = 85, so the score drops from 100 to 15. The fifth rule, the forward-chain penalty, does not apply: the message was forwarded only 3 times, and the rule requires 5 or more hops before it fires. Treating any forwarding as enough to trigger that rule (subtracting the extra 15 anyway) is a common misreading of "IF forwarded through >= 5 hops" and produces an incorrect score of 0. Skipping the fact-checker-contradiction penalty entirely — for example, assuming only an official government retraction counts, not independent fact-checkers — gives 40, undercounting a genuine warning sign. Skipping both that penalty and the unverified-publisher penalty gives 70, which not only misstates the score but also flips the label from "Likely Misinformation" to the milder "Verify Before Sharing," even though two of the strongest evidence-based red flags (an unlisted publisher and direct contradiction by fact-checkers) are present. Since 15 is below the 60-point cutoff, "Likely Misinformation" is the correct label, and the key critical-thinking lesson is that sensational language and a young domain are weaker signals on their own than being contradicted by named fact-checking organisations and lacking any verifiable publisher.

Question 169 · Regular Expressions: Pattern Matching Power · hard

Consider this Python code: ```python import re s = "<b>Bold</b> and <i>Italic</i>" pattern = r'<.+>' match = re.search(pattern, s) print(match.group()) ``` What does this code print?

  1. <b>Bold</b> and <i>Italic</i>
  2. <b>
  3. <b>Bold</b>
  4. match is None, because .+ cannot match across multiple separate <...> tag pairs in one string

Answer: A. <b>Bold</b> and <i>Italic</i>

ExplanationThe quantifier `+` in `.+` is greedy by default, which means the engine does not stop at the first opportunity to satisfy the rest of the pattern — it first tries to consume as many characters as possible (everything up to the end of the string), and only then backtracks, one character at a time, until whatever comes next in the pattern (here, the literal `>`) can finally match. Trace it: `re.search` anchors the attempt at the first `<` (index 0). `.+` grabs the rest of the string greedily: `b>Bold</b> and <i>Italic</i`. The pattern still needs a trailing `>`, so the engine backtracks from the end — but the very last character of `s` already is `>` (the close of `</i>`), so it only has to give back that one character before the match succeeds. It never needs to backtrack further, so it never gets a chance to stop at the `>` right after `<b`. The net effect: `.+` swallows straight through `Bold</b> and <i>Italic</i`, and `match.group()` returns the whole string, `<b>Bold</b> and <i>Italic</i>` — not just the first tag. This is exactly why greedy quantifiers are dangerous around repeated delimiters like HTML tags: to make the match stop at the nearest `>` instead, you would need the lazy version `.+?`, which tries to match as little as possible before checking whether the rest of the pattern can succeed.

Question 170 · Searching Algorithms: Finding Needles in Haystacks · hard

A school office keeps a list of roll numbers for a scholarship verification queue. The clerk assumes the list is sorted and runs standard binary search to check whether roll number 8 is present: ``` index: 0 1 2 3 4 5 6 value: 15 42 8 23 91 4 67 ``` The array is actually NOT sorted. If the clerk runs textbook binary search (low=0, high=6, mid=(low+high)//2, comparing arr[mid] to the target 8) to search for 8, what actually happens?

  1. Binary search correctly finds 8 at index 2, just taking a few more comparisons than a properly sorted array would need.
  2. Binary search compares against 23, then 42, then 15 — discarding index 2 along the way — and terminates reporting "not found", even though 8 is sitting right there at index 2.
  3. Binary search runs into an infinite loop, because the low and high pointers never manage to cross when the underlying array isn't sorted.
  4. Binary search successfully locates the value 8, but reports the wrong index because the midpoint calculation assumes sorted order.

Answer: B. Binary search compares against 23, then 42, then 15 — discarding index 2 along the way — and terminates reporting "not found", even though 8 is sitting right there at index 2.

ExplanationTrace it exactly as the algorithm would run. Array: index 0=15, 1=42, 2=8, 3=23, 4=91, 5=4, 6=67. Target = 8. Step 1: low=0, high=6, mid=(0+6)//2=3, arr[3]=23. Since 8<23, the algorithm assumes the target — if present — must lie strictly to the left, so it sets high=mid-1=2. Range is now indices 0–2, which still includes index 2 (value 8), so nothing has gone wrong yet. Step 2: low=0, high=2, mid=(0+2)//2=1, arr[1]=42. Since 8<42, high becomes mid-1=0. This is the fatal step: the new range is just index 0, so index 2 — where 8 actually lives — gets thrown away. Binary search "trusts" that anything below index 1 with a smaller value than 42 can't come after position 1, which is only a valid rule when the array is genuinely sorted ascending. Here it isn't: 8 sits at index 2, right after 42 at index 1, breaking that assumption. Step 3: low=0, high=0, mid=0, arr[0]=15. Since 8<15, high becomes -1. Now low(0) > high(-1), so the loop ends and the algorithm reports "not found". So the search compared against 23, then 42, then 15 — in exactly that order — and concluded 8 isn't present, despite it sitting at index 2 the entire time. This is the real danger of running binary search on unsorted data: it doesn't just get slower (like the infinite-loop misconception suggests, which is false — low and high do cross and the loop terminates cleanly), and it doesn't find the value at a wrong index either. It can silently return a false negative on data that's actually present, because every halving step depends entirely on the sorted-order guarantee. This is exactly why real systems (like a database index or an IRCTC seat-availability lookup) must guarantee sortedness before ever applying binary search — an unsorted binary search isn't a slower correct algorithm, it's a fast incorrect one.

Question 171 · Introduction to Graphs: Networks and Connections · hard

A school computer lab sets up a Wi-Fi mesh network with 6 routers, where each router is a vertex and every direct cable between two routers is an edge. The network has exactly 9 direct cable connections in total. Five of the six routers have degrees (number of direct cable connections) 3, 4, 2, 5, and 3. What must be the degree of the sixth router?

  1. 1
  2. 2
  3. 3
  4. 9

Answer: A. 1

ExplanationEvery cable in the mesh network is an edge, and each edge touches exactly two routers, adding 1 to the degree count of each of those two routers. This means the sum of the degrees of all vertices in any network always equals twice the number of edges — this is the Handshake Lemma, a core fact about graphs. With 9 direct connections in this network, the total degree sum must be 2 x 9 = 18. Adding up the five known degrees gives 3 + 4 + 2 + 5 + 3 = 17. So the sixth router's degree is 18 - 17 = 1: it has exactly one direct cable connection into the rest of the mesh. Getting 2 usually comes from mis-adding the five known degrees as 16 instead of 17. Getting 9 comes from confusing the total number of edges with the missing vertex's own degree, skipping the doubling step entirely. Getting 3 comes from assuming the missing router should just match the most common degree in the list rather than actually working out the total from the edge count.

Question 172 · Data Analysis with Pandas · hard

A student is analysing sales data (in rupees) from two Indian cities using pandas. One reading is missing from the dataset. Study the code below, then work out the exact value that gets printed. ```python import pandas as pd data = { 'city': ['Delhi', 'Mumbai', 'Delhi', 'Mumbai', 'Delhi'], 'sales': [200, 150, None, 300, 100] } df = pd.DataFrame(data) result = df.groupby('city')['sales'].mean() print(result['Delhi']) ``` What value does this code print?

  1. 150.0, because pandas' groupby mean() ignores the missing value and averages only the two valid entries
  2. 100.0, because pandas treats the missing value as zero before computing the average of three entries
  3. NaN, because a missing value makes any aggregate calculation on that group undefined
  4. 200.0, because groupby().mean() returns the first recorded sales value for each city group

Answer: A. 150.0, because pandas' groupby mean() ignores the missing value and averages only the two valid entries

ExplanationDelhi appears in three rows of the DataFrame, with sales values 200, a missing entry, and 100. pandas' groupby().mean() runs with skipna=True by default, so it does not zero-fill or crash on the missing entry — it simply excludes that row from both the sum and the count for the group. That leaves two valid numbers for Delhi, 200 and 100, which sum to 300; dividing by 2 (the count of valid entries, not 3) gives 150.0, exactly what result['Delhi'] prints. The 100.0 trap comes from wrongly treating the gap as a zero and dividing by all three rows (200 + 0 + 100 = 300, divided by 3); NaN would only be the output if skipna were explicitly turned off with mean(skipna=False); and 200.0 confuses an aggregation like mean() with simply grabbing the first value stored in the group.

Question 173 · Web Scraping with BeautifulSoup · hard

An IRCTC-style train search results page produces this HTML, generated by the booking site's template: ```html <div class="train-list"> <div class="available"> <span class="name">Rajdhani Express</span> <span class="price">₹1450</span> </div> <div class="train-card available"> <span class="name">Shatabdi Express</span> <span class="price">₹980</span> </div> <div class="available premium"> <span class="name">Duronto Express</span> <span class="price">₹2100</span> </div> <div class="availableSoon"> <span class="name">Vande Bharat Express</span> <span class="price">₹1800</span> </div> </div> ``` A student scrapes this page with the following BeautifulSoup code: ```python from bs4 import BeautifulSoup soup = BeautifulSoup(html, "html.parser") matches = soup.find_all("div", class_="available") ``` How many div tags does matches actually contain, and why?

  1. matches contains 4 div tags, because class_='available' performs a substring match against the class attribute, so 'availableSoon' also counts as a match since it starts with the text 'available'
  2. matches contains 1 div tag, because class_='available' only matches when the class attribute equals exactly the single word 'available', with no other class names present in that tag
  3. matches contains 3 div tags, because BeautifulSoup treats a multi-valued class attribute as a list of separate class names and counts a match whenever 'available' appears anywhere in that list, not only when it is listed first
  4. matches contains 2 div tags, because BeautifulSoup only counts a match when 'available' is the first class name listed inside the tag's class attribute

Answer: C. matches contains 3 div tags, because BeautifulSoup treats a multi-valued class attribute as a list of separate class names and counts a match whenever 'available' appears anywhere in that list, not only when it is listed first

ExplanationBeautifulSoup does not store a tag's class attribute as one single string — it splits it on whitespace into a list of separate class tokens, and class_='available' matches a tag if 'available' appears anywhere in that list, regardless of position or of what other classes sit alongside it. Checking all four div tags: class="available" has the token list ["available"], which contains "available", so it matches. class="train-card available" has tokens ["train-card", "available"] — "available" is present even though it isn't the first token, so it matches too. class="available premium" has tokens ["available", "premium"], which also contains "available", so it matches. class="availableSoon" has just one token, "availableSoon", and that token is not equal to "available" — BeautifulSoup compares whole tokens, not substrings or prefixes, so this div is excluded. That gives 3 matching div tags in total. The idea that a substring or prefix match happens is a common mix-up with how CSS attribute selectors like [class^="available"] work, but plain class_ matching in BeautifulSoup never does partial-word matching — 'availableSoon' and 'available' are simply different tokens. Requiring the class attribute to be exactly the single word 'available' is also incorrect, since BeautifulSoup treats a multi-valued class attribute as a list and checks membership in that list rather than comparing it as one combined string, so extra classes on the same tag don't disqualify a match. Finally, position inside the class attribute is irrelevant — BeautifulSoup checks whether 'available' is present anywhere in the token list, not only when it happens to be listed first.

Question 174 · Testing and Debugging Python Code · hard

A student wrote this function to compute the average of exam marks, then added an `assert` statement as a test case: ```python def average_marks(marks): total = 0 for i in range(len(marks) - 1): total += marks[i] return total / len(marks) assert average_marks([72, 85, 90, 68, 95]) == 82.0 ``` When this code runs, an `AssertionError` fires because the function's actual return value does not match 82.0. Tracing through the loop by hand, what value does `average_marks([72, 85, 90, 68, 95])` actually return?

  1. 63.0
  2. 82.0
  3. 78.75
  4. 102.5

Answer: A. 63.0

ExplanationThe loop condition `range(len(marks) - 1)` is the bug. For the 5-element list, `len(marks) - 1` is 4, so `range(4)` only produces the indices 0, 1, 2, 3 — the loop body never runs for `i = 4`, and `marks[4]` (the mark 95) is silently skipped. So `total` only accumulates `marks[0] + marks[1] + marks[2] + marks[3] = 72 + 85 + 90 + 68 = 315`. The return line then divides by `len(marks)`, which is still the true length, 5 (this part of the code was never shortened) — giving `315 / 5 = 63.0`. That rules out 78.75, which is what you'd get only if the denominator had also been reduced to 4 (`315 / 4`), and it rules out 102.5, which comes from mixing the full correct sum of 410 with the shortened denominator of 4 (`410 / 4`). It also rules out 82.0, the true average (`410 / 5`) a reader gets only by assuming the function has no bug at all. Because the `assert` statement compares the real return value, 63.0, against the expected 82.0, the assertion fails — and that failure is exactly what points a tester to the classic off-by-one mistake: `range(len(marks) - 1)` should have been `range(len(marks))` so every mark, including the last one, gets added in.

Question 175 · Design Patterns for Beginners · hard

Study this attempt at the Singleton pattern in Python: ```python class Counter: _instance = None def __new__(cls): if cls._instance is None: cls._instance = super().__new__(cls) return cls._instance def __init__(self): self.count = 0 c1 = Counter() c1.count = 5 c2 = Counter() print(c2.count) ``` What value does `print(c2.count)` output, and why?

  1. 5 — because c2 refers to the same object as c1, and __init__ is skipped once the instance already exists
  2. 0 — because __init__ runs again after __new__ returns the existing instance, resetting count back to 0
  3. 0 — because __new__ creates a brand new Counter object each time, so c2 is unrelated to c1
  4. 5 — because Python's Singleton pattern guarantees __init__ only executes on the very first call to the class

Answer: B. 0 — because __init__ runs again after __new__ returns the existing instance, resetting count back to 0

ExplanationCreating an object in Python is a two-step process: `__new__` builds (or fetches) the object, and then Python automatically calls `__init__` on whatever `__new__` returned — as long as that return value is an instance of the class. This second step is the part beginners miss when they write a Singleton this way. Trace it carefully. When `c1 = Counter()` runs, `cls._instance` is `None`, so `__new__` creates a fresh object and stores it in `cls._instance`. Because that returned object is a `Counter` instance, Python then calls `__init__` on it, setting `self.count = 0`. The line `c1.count = 5` changes that same object's `count` to 5. Now `c2 = Counter()` runs. Inside `__new__`, `cls._instance` is no longer `None`, so the `if` block is skipped and the *existing* object is returned — this part of the Singleton logic works correctly, and `c2` is genuinely the same object as `c1` (`c1 is c2` would be `True`). But `__init__` does not know or care that this object already existed. Python calls `__init__` again simply because `__new__` handed back a `Counter` instance, so `self.count = 0` runs a second time, overwriting the 5 with 0 on that shared object. So `print(c2.count)` outputs 0 — but not because `c2` is a separate, unrelated object (it isn't), and not because Python skips re-initialization for existing instances (it doesn't skip it at all). A correct Singleton implementation must guard `__init__` itself, for example with `if not hasattr(self, "count"): self.count = 0`, otherwise every "new" call quietly resets state — a bug that defeats the whole purpose of the pattern.

Question 176 · Generators and Iterators: Lazy Evaluation · hard

Consider this generator function and the calls made to it: ```python def gen(): print("A") yield 1 print("B") yield 2 print("C") yield 3 g = gen() print("start") x = next(g) print("got", x) y = next(g) print("got", y) ``` What is the exact sequence of lines printed to the console, in order?

  1. A B C start got 1 got 2
  2. start A B got 1 got 2
  3. start A got 1 B got 2
  4. start A got 1 A B got 2

Answer: C. start A got 1 B got 2

ExplanationThe line `g = gen()` only creates a generator object — none of the code inside `gen` runs yet, because a function containing `yield` doesn't execute when called, it just returns a paused iterator. So `print("start")` fires first, with no "A" before it. The first `next(g)` call is what actually starts running the function body: it executes `print("A")`, then hits `yield 1`, which freezes execution right there and hands back 1 as `x`. That's why "got 1" follows "A". Crucially, the generator does not restart from the top on the next call — it resumes exactly where it froze. So the second `next(g)` continues immediately after `yield 1`, runs `print("B")`, then hits `yield 2` and freezes again, giving `y = 2` and printing "got 2". Putting it together: `start`, `A`, `got 1`, `B`, `got 2` — five lines, each print statement firing only when execution actually reaches it, never before the generator is asked to advance and never repeated. `print("C")` never runs at all, since a third `next(g)` was never called. The other sequences reflect two classic misconceptions: assuming the generator body runs eagerly the moment `gen()` is called (producing "A", "B", "C" before "start" even prints), and assuming each `next()` call restarts the function from its first line instead of resuming from the last `yield` (which would reprint "A" on the second call).

Question 177 · Decorators · hard

A programmer writing a small utility library defines two decorators, then stacks them on a function that squares a number. Trace the execution carefully and find what gets printed. ```python def double(func): def wrapper(x): return 2 * func(x) return wrapper def add_one(func): def wrapper(x): return func(x) + 1 return wrapper @double @add_one def square(x): return x * x print(square(3)) ``` What value does this code print?

  1. 20 — add_one wraps square first, turning a call into square(3) + 1 = 9 + 1 = 10, and then double wraps that result, giving 2 × 10 = 20.
  2. 19 — double wraps square first, turning a call into 2 × square(3) = 2 × 9 = 18, and then add_one adds 1 to get 19.
  3. 18 — only the double decorator actually takes effect, giving 2 × square(3) = 2 × 9 = 18, since applying add_one overwrites it.
  4. 10 — only the add_one decorator actually takes effect, giving square(3) + 1 = 9 + 1 = 10, since applying double overwrites it.

Answer: A. 20 — add_one wraps square first, turning a call into square(3) + 1 = 9 + 1 = 10, and then double wraps that result, giving 2 × 10 = 20.

ExplanationStacked decorators apply from the function outward, not from top to bottom in reading order. The decorator written directly above the function — add_one — wraps the original square first, so square becomes equivalent to add_one(original_square). Only after that does double wrap the already-wrapped version, so square finally becomes double(add_one(original_square)). Calling square(3) therefore runs double's wrapper, which calls add_one's wrapper, which calls the original square: original_square(3) gives 3 × 3 = 9, add_one's wrapper adds 1 to get 9 + 1 = 10, and double's wrapper doubles that to 2 × 10 = 20. Reading the decorators in the wrong order — treating double as if it ran first — is the classic trap, and it produces 2 × 9 + 1 = 19 instead. Assuming one decorator simply overwrites the other, rather than each wrapping the previous result, gives the remaining incorrect values of 18 or 10.

Question 178 · Hash Tables · hard

A school's fee-payment tracker stores UPI payment reference numbers in a hash table with 7 buckets (indices 0 to 6), using the hash function h(k) = k mod 7. Collisions are resolved by linear probing: if a bucket is occupied, the table checks the next index, wrapping from index 6 back to index 0. The reference numbers arrive in this order: 23, 15, 9, 40, 16. At which index does the reference number 16 finally get stored?

  1. Index 2
  2. Index 3
  3. Index 4
  4. Index 5

Answer: C. Index 4

ExplanationTrace the insertions one at a time, computing h(k) = k mod 7 and probing forward whenever a bucket is already taken. - 23 mod 7 = 2 (since 7×3 = 21, remainder 2) → bucket 2 is empty → 23 goes to index 2. - 15 mod 7 = 1 (7×2 = 14, remainder 1) → bucket 1 is empty → 15 goes to index 1. - 9 mod 7 = 2 → bucket 2 already holds 23 (collision) → probe index 3, which is empty → 9 goes to index 3. - 40 mod 7 = 5 (7×5 = 35, remainder 5) → bucket 5 is empty → 40 goes to index 5. - 16 mod 7 = 2 → bucket 2 holds 23 (collision) → probe index 3, which holds 9 (second collision) → probe index 4, which is empty → 16 goes to index 4. So the final table is: index 1 → 15, index 2 → 23, index 3 → 9, index 4 → 16, index 5 → 40, with indices 0 and 6 empty. Key 16 needed two probes past its natural home bucket before finding a free slot, landing at index 4. Index 2 is only where 16's hash function *starts* looking — it's the home bucket for 23, not 16's final resting place, so stopping there ignores the two collisions that follow. Index 3 is where the previous key, 9, ended up after its own single collision — mixing up 9's final index with 16's is an easy slip since both keys collide with 23 in the same bucket. Index 5 overshoots by one step, as if 16 had to probe past 40's bucket too, but 40 sits at index 5 (its own natural hash) and was never in 16's probing path — 16's probe sequence only touches indices 2, 3, and 4 before finding the empty slot.

Question 179 · Unit Testing · hard

An Indian e-commerce site's checkout module must follow this rule: orders worth ₹1000 or more get a 10% discount, and members get an extra 5% off on top (the two discounts are added as flat percentages of the original price — they do not compound). A developer writes this JavaScript function for it: ```js function calculateDiscount(price, isMember) { let discount = 0; if (price > 1000) { discount = 0.10; } if (isMember) { discount += 0.05; } return price - (price * discount); } ``` Four unit test assertions are written to check this function against the stated rule. Tracing the code by hand for each input, which one of these assertions will actually FAIL when the test suite runs?

  1. Testing the ₹1000 boundary with a non-member order, the assertion expects `calculateDiscount(1000, false)` to equal ₹900.
  2. Testing a clearly-above-threshold order from a non-member, the assertion expects `calculateDiscount(1500, false)` to equal ₹1350.
  3. Testing a below-threshold order placed by a member, the assertion expects `calculateDiscount(500, true)` to equal ₹475.
  4. Testing a well-above-threshold order placed by a member, the assertion expects `calculateDiscount(2000, true)` to equal ₹1700.

Answer: A. Testing the ₹1000 boundary with a non-member order, the assertion expects `calculateDiscount(1000, false)` to equal ₹900.

ExplanationThe bug is a classic boundary-value (off-by-one) error: the spec says "₹1000 or more" (price >= 1000), but the code tests `price > 1000`, a strictly-greater comparison that silently excludes the exact value ₹1000. Tracing `calculateDiscount(1000, false)`: `1000 > 1000` is false, so `discount` stays 0, and since `isMember` is false nothing is added. The function returns `1000 - (1000 * 0) = ₹1000`. The test asserts the result should be ₹900 (10% off a ₹1000 order), so the assertion fails — the code and the spec disagree exactly at the boundary. The other three tests all pass, but not because the code is correct — it's because none of them sits on the boundary, so the `>` vs `>=` bug never gets triggered: - `calculateDiscount(1500, false)`: `1500 > 1000` is true, discount = 0.10, result = `1500 - 150 = ₹1350`, matching the expected ₹1350. - `calculateDiscount(500, true)`: `500 > 1000` is false (discount 0), `isMember` adds 0.05, result = `500 - 25 = ₹475`, matching the expected ₹475. - `calculateDiscount(2000, true)`: `2000 > 1000` is true (discount 0.10), plus member's 0.05 = 0.15, result = `2000 - 300 = ₹1700`, matching the expected ₹1700. This is exactly why unit testing relies on boundary value analysis: picking test inputs safely above or below a threshold can make a suite look green even though a single off-by-one mistake breaks behavior right at the edge. A thorough test suite must always include the exact threshold value itself, not just values comfortably on either side of it.

Question 180 · Web Scraping · hard

DesiMart, an Indian e-commerce site, publishes this robots.txt file to control what scrapers may crawl: ``` User-agent: * Disallow: /products/ Allow: /products/sale/ Disallow: /products/sale/limited/ ``` Robots.txt rules are not applied in file order. A crawler compares a URL's path against every rule whose path string it starts with, and whichever matching rule has the longest path string wins. Based on this, which one of the following paths on desimart.in is a compliant scraper actually permitted to crawl?

  1. /products/electronics/mobiles
  2. /products/sale/summer-collection
  3. /products/sale/limited/diwali-offer
  4. /products/sale

Answer: B. /products/sale/summer-collection

ExplanationRobots.txt matching works on longest-prefix-wins, not on which line appears first in the file. Check /products/sale/summer-collection against each rule: it starts with "/products/" (10 characters, a Disallow) and it also starts with "/products/sale/" (15 characters, an Allow) — but it does not start with "/products/sale/limited/" (23 characters), since the text after /products/sale/ is "summer-collection", not "limited/". Between the two rules that do match, 15 characters beats 10, so the Allow rule wins and this path can be crawled. The other three paths all end up blocked. /products/electronics/mobiles never reaches the word "sale" at all, so the only rule it matches is the 10-character Disallow: /products/ — blocked. /products/sale/limited/diwali-offer matches all three rules (10, 15, and 23 characters), but the longest match is the 23-character Disallow: /products/sale/limited/, which overrides the shorter Allow rule — blocked. /products/sale itself is only 14 characters long with no trailing slash, so it cannot start with the 15-character string "/products/sale/" (a path can't match a prefix that is longer than the path itself); it only matches the 10-character Disallow: /products/ rule, so it's blocked too.
← Set 8Set 10 →