Chapter 8 Quiz: Error Handling
Instructions: Choose the best answer for each multiple-choice question. Short-answer questions should be answered in 1–3 sentences. Code questions require working Python 3.10+ syntax.
Part A: Multiple Choice (Questions 1–10)
Question 1
Priya's script runs on a scheduled task every morning. When it encounters an unexpected problem, which approach produces the most useful outcome?
A) Allow the script to crash with a full traceback
B) Catch all exceptions with except: and print "Error occurred"
C) Catch specific exceptions, log detailed messages, and continue processing where possible
D) Wrap everything in try/except Exception: pass to prevent any errors from appearing
Question 2
Which of the following is the correct Python exception type for attempting to access a dictionary key that does not exist?
A) IndexError
B) KeyError
C) LookupError
D) AttributeError
Question 3
Examine this code:
try:
result = float("pending approval")
except ValueError:
result = 0.0
else:
print(f"Conversion succeeded: {result}")
finally:
print("Validation step complete.")
If "pending approval" is passed, which statements will be printed?
A) "Conversion succeeded: 0.0" and "Validation step complete." B) "Validation step complete." only C) Nothing is printed — the exception prevents all output D) "Conversion succeeded: 0.0" only
Question 4
What is the primary reason to avoid using a bare except: clause?
A) It runs slower than specific exception clauses
B) It also catches KeyboardInterrupt and SystemExit, and it hides programming bugs by catching exceptions you did not intend to handle
C) Python raises a SyntaxError if you use a bare except:
D) It only works in Python 2 and is deprecated in Python 3
Question 5
Maya writes this code to handle invoice data:
try:
hours = float(row["hours"])
rate = float(row["rate"])
amount = hours * rate
except (ValueError, KeyError):
amount = 0.0
What does grouping ValueError and KeyError in a tuple accomplish?
A) It means the except clause handles only the first exception type that occurs B) Both exception types are handled by the same except clause and the same recovery code C) This is a syntax error — you cannot group exception types in a tuple D) It creates two separate except clauses that run sequentially
Question 6
Which log level from the logging module is most appropriate for the message "Skipping row 47: hours field contains 'N/A'"?
A) logging.DEBUG
B) logging.INFO
C) logging.WARNING
D) logging.CRITICAL
Question 7
What does the finally clause guarantee?
A) The finally block runs only when no exception was raised
B) The finally block runs only when an exception was raised and caught
C) The finally block always runs, whether an exception was raised or not
D) The finally block suppresses any exceptions that occurred in the try block
Question 8
What is wrong with this custom exception definition?
class InvoiceError:
def __init__(self, message):
self.message = message
A) Custom exceptions must use class InvoiceError(Exception): — they must inherit from Exception or a subclass
B) The __init__ method should not take arguments
C) Custom exceptions cannot store attributes
D) Nothing is wrong — this is a valid exception class
Question 9
In the logging module, which method should you use when you want to log an error message AND automatically include the full exception traceback?
A) logger.error(message, traceback=True)
B) logger.exception(message)
C) logger.error(message, include_stack=True)
D) logger.trace(message)
Question 10
Marcus is reviewing Priya's refactored code. He sees she has structured a loop like this:
for region in REGIONS:
try:
process_region(region)
except Exception as e:
logger.error(f"Region {region} failed: {e}")
What does this structure accomplish that a try/except block outside the loop would not?
A) It runs faster because exceptions are handled closer to where they occur B) An error in one region does not prevent the other regions from being processed C) It catches more types of exceptions than a try/except outside the loop D) It makes the code easier to read but has no functional difference
Part B: True / False (Questions 11–15)
For each statement, write True or False and give a one-sentence explanation.
Question 11
If you write except LookupError:, your except clause will catch both KeyError and IndexError exceptions.
Question 12
The else clause in a try/except/else block runs only if an exception was caught.
Question 13
A bare raise statement (with no arguments) inside an except block re-raises the exception that was just caught, preserving the original traceback.
Question 14
Using logger.warning() instead of logger.error() for a skipped row is always wrong — any skipped row represents an error and should be logged at ERROR level.
Question 15
The logging.basicConfig() function should typically be called once, near the top of your main script, not inside functions or loops.
Part C: Code Analysis (Questions 16–18)
Read each code snippet carefully and answer the questions below it.
Question 16
def calculate_margin(revenue, cost):
try:
margin = (revenue - cost) / revenue
except ZeroDivisionError:
margin = 0.0
except ValueError:
margin = 0.0
else:
if margin < 0:
print(f"Warning: negative margin {margin:.1%}")
return margin
a) Under what condition does the else block execute?
b) Under what condition would a ZeroDivisionError be raised?
c) What is the return value if revenue = 0 and cost = 5000?
d) What is the return value if revenue = 80000 and cost = 50000?
Question 17
class PaymentError(Exception):
pass
class InsufficientFundsError(PaymentError):
def __init__(self, account_id, balance, required):
self.account_id = account_id
self.balance = balance
self.required = required
super().__init__(
f"Account {account_id}: insufficient funds "
f"(balance: ${balance:.2f}, required: ${required:.2f})"
)
def charge_account(account_id, balance, amount):
if balance < amount:
raise InsufficientFundsError(account_id, balance, amount)
return balance - amount
a) Write a try/except block that calls charge_account("ACC-001", 500.00, 750.00) and catches the exception, printing the account ID and the shortfall amount.
b) Would except PaymentError: catch an InsufficientFundsError? Explain.
c) What does super().__init__(...) do in the InsufficientFundsError class?
Question 18
import logging
logger = logging.getLogger(__name__)
def load_and_process(filepath):
data = []
try:
with open(filepath, encoding="utf-8") as f:
import csv
reader = csv.DictReader(f)
for row in reader:
try:
amount = float(row["amount"])
data.append(amount)
logger.debug(f"Parsed ${amount:.2f}")
except (ValueError, KeyError) as e:
logger.warning(f"Bad row: {e}")
except FileNotFoundError:
logger.error(f"File not found: {filepath}")
except PermissionError:
logger.error(f"Cannot read: {filepath}")
finally:
logger.info(f"load_and_process finished. {len(data)} values loaded.")
return data
a) Identify the two levels of error handling in this function.
b) What happens to data if filepath does not exist? What does the function return?
c) When is the finally block's log message printed?
d) If one row has "amount": "N/A", what happens to the rest of the rows?
Part D: Short Answer (Questions 19–20)
Question 19
Explain the concept of "graceful degradation" in the context of a business data processing script. Use a concrete example from either the Acme Corp or Maya Reyes scenarios. Your answer should address:
- What "graceful degradation" means
- Why it is preferable to the alternative
- What must be true about error logging for graceful degradation to be useful
Question 20
Compare using print() statements versus the logging module for error output in a production script. Give at least three specific advantages of logging over print(), and describe a scenario from the chapter where each advantage would matter.
Answer Key
Part A: Multiple Choice
Q1: C — Catch specific exceptions, log detailed messages, and continue processing where possible. This produces partial output (useful) and a log explaining what failed (actionable). Option A leaves the operator with nothing; option B produces an uninformative message; option D silently hides all problems.
Q2: B — KeyError is raised when you access a dictionary with a key that does not exist. IndexError is for list index out of range. LookupError is the parent class of both and can be used to catch either, but is not the specific type raised.
Q3: B — Only "Validation step complete." is printed. Because float("pending approval") raises ValueError, the except ValueError block runs (setting result = 0.0) and the else block is skipped (it only runs when no exception occurred). The finally block always runs, so its print() executes.
Q4: B — A bare except: catches KeyboardInterrupt (making the script impossible to stop with Ctrl+C) and SystemExit, and — more commonly — it catches NameError, AttributeError, and other programming bugs that should be visible rather than silently swallowed. It does not cause a SyntaxError.
Q5: B — Grouping exception types in a tuple means both types are handled by the same except clause. When either a ValueError or a KeyError is raised, the same recovery code (amount = 0.0) runs. This is equivalent to two separate except clauses that execute the same code.
Q6: C — logging.WARNING. The script can continue; no data has been lost (yet); but something unexpected happened that someone should review. DEBUG is for developer diagnostics; INFO is for normal operations; ERROR and CRITICAL imply something more serious than a skipped row.
Q7: C — The finally block always runs, whether the try block completed normally, whether an exception was caught by an except clause, or even whether an uncaught exception is propagating upward. This is why it is used for cleanup (closing files, releasing connections).
Q8: A — The class must inherit from Exception (or a subclass of Exception). Without inheritance, Python will not treat it as a true exception and you cannot raise it with raise InvoiceError(...) in a way that integrates with the exception system.
Q9: B — logger.exception(message) logs at ERROR level and automatically appends the full traceback of the currently-handled exception. It is equivalent to logger.error(message, exc_info=True).
Q10: B — The try/except inside the loop creates an error boundary at the level of a single region. If processing Region A fails, the except clause runs, the loop moves to the next iteration, and Region B is processed. A try/except outside the loop would catch the first failure and stop all subsequent processing.
Part B: True / False
Q11: True. KeyError and IndexError are both subclasses of LookupError. Because Python's exception catching works by checking the exception hierarchy, except LookupError: catches any exception that is a LookupError or a subclass of it — including both KeyError and IndexError.
Q12: False. The else clause runs only if no exception was raised in the try block. It is the "success path" — code that only makes sense if everything in the try block worked.
Q13: True. A bare raise (with no arguments) inside an except block re-raises the exception currently being handled, preserving its original type, message, and traceback. This is used when you want to log an error but still propagate it to the caller.
Q14: False. Log levels should reflect the severity of the impact on the overall process. A skipped row in a batch of 500 is a warning — the script can continue and the overall output is only slightly degraded. WARNING is appropriate. Reserve ERROR for situations where a whole file or major component has failed.
Q15: True. basicConfig() configures the root logger and should be called once at program startup. Calling it inside a function or loop can cause duplicate handlers to be added on every call, resulting in duplicate log messages. Configuring logging near the top of your main script keeps the configuration in one place and ensures it runs before any logging happens.
Part C: Code Analysis
Q16:
a) The else block executes when no exception was raised in the try block — i.e., when the division succeeded without a ZeroDivisionError or ValueError.
b) A ZeroDivisionError is raised when revenue equals 0 (division by zero).
c) 0.0 — the ZeroDivisionError is caught and margin is set to 0.0; the else block does not run.
d) 0.375 — (80000 - 50000) / 80000 = 0.375. No exception is raised, so the else block runs and would print a warning only if margin were negative; since 0.375 > 0, nothing is printed.
Q17: a)
try:
new_balance = charge_account("ACC-001", 500.00, 750.00)
except InsufficientFundsError as e:
shortfall = e.required - e.balance
print(f"Account: {e.account_id}, Shortfall: ${shortfall:.2f}")
b) Yes. InsufficientFundsError inherits from PaymentError, so except PaymentError: will catch it (because catching a parent class catches all subclasses). This is useful when you want to handle any payment-related error the same way.
c) super().__init__(...) calls the Exception class's __init__ method with the message string. This sets the string representation of the exception — what you see when you print(exc) or when Python displays the traceback.
Q18:
a) There are two levels: (1) the outer try/except handles file-level failures (FileNotFoundError, PermissionError) — the whole file cannot be accessed; (2) the inner try/except inside the for loop handles row-level failures (ValueError, KeyError) — individual rows can fail without stopping the file processing.
b) data remains [] (empty). The FileNotFoundError is caught, logged, and the with block is never entered. The function returns [].
c) The finally block runs in all cases: after a successful file load, after a FileNotFoundError, after a PermissionError, and even if an unexpected exception propagated out of the outer try block. It runs as the last thing before the function returns (or before the exception propagates further).
d) The ValueError raised by float("N/A") is caught by the inner except clause, a warning is logged, and the for loop continues to the next row. All other rows are processed normally.
Part D: Short Answer
Q19 — Sample Answer: Graceful degradation means designing a system to produce partial, useful output when some part of the input is problematic, rather than producing no output at all. In the Priya case study, the original script produced zero output when even one row contained "N/A" — the total failure is worse than the partial failure. The refactored script continues processing the other three regions and skips bad rows within the Western file, producing a report with an explicit note that some data was excluded. This is preferable because the operations team can act on partial data while the data quality issues are resolved, rather than waiting for a complete re-run. For graceful degradation to be useful, every skipped item must be logged with a specific reason — otherwise the partial output is misleading (the consumer does not know it is incomplete or why).
Q20 — Sample Answer: The logging module offers at least these advantages over print(): (1) Severity levels — with print(), all messages look the same; with logging, a WARNING is visually distinct from DEBUG. In a long log output, you can instantly see which lines represent problems. (2) Automatic timestamps — logging can be configured to prefix every message with the exact date and time, which is essential when a scheduled job ran unattended at 3 AM and you need to know when an error occurred. (3) Output destinations — logging can simultaneously write to the console, a log file, and even a remote log server, with different severity thresholds for each. print() can only write to the console (or a redirected stream). (4) Tracebacks on demand — logger.exception() automatically captures the full traceback of the currently-handled exception without any extra code. With print(), you would need to import traceback and call traceback.format_exc() manually. In Priya's Monday report scenario, timestamps matter because the report runs before anyone is at their desk — knowing whether the error occurred at 6:47 AM (the file wasn't there yet) or 6:52 AM (the file arrived but was corrupt) could significantly change the diagnosis.