Quiz: Error Handling

Test your understanding before moving on. Target: 70% or higher to proceed confidently.


Section 1: Multiple Choice (1 point each)

1. Which of the following is a runtime error (exception), not a syntax error?

  • A) print("hello" (missing closing parenthesis)
  • B) int("abc")
  • C) def greet(name) (missing colon)
  • D) x = = 5 (double equals in assignment)
Answer **B)** `int("abc")` *Why B:* This is syntactically valid Python, but at runtime `int()` cannot convert the string `"abc"` to an integer, so it raises `ValueError`. *Why not A, C, D:* These are all syntax errors — Python catches them during parsing before the program runs. *Reference:* Section 11.2

2. When reading a Python traceback, you should start reading from:

  • A) The top (first line)
  • B) The bottom (last line)
  • C) The middle
  • D) It doesn't matter — read in any order
Answer **B)** The bottom (last line) *Why B:* The bottom line shows the exception type and message — what went wrong. The lines above it show the call chain in order from oldest (top) to most recent (bottom). Starting at the bottom gives you the most actionable information first. *Reference:* Section 11.3

3. What happens if an exception occurs inside a try block and no except clause matches it?

  • A) Python silently ignores the exception
  • B) The else block runs
  • C) The exception propagates up to the caller (but finally still runs if present)
  • D) Python automatically catches it and prints a generic message
Answer **C)** The exception propagates up to the caller (but `finally` still runs if present) *Why C:* Unmatched exceptions are not silenced — they propagate up the call stack. If no handler catches them anywhere, the program crashes with a traceback. The `finally` block still executes before propagation. *Why not A:* Python never silently ignores unhandled exceptions. *Why not B:* The `else` block only runs when NO exception occurs. *Reference:* Section 11.6

4. What does the else clause in a try/except/else block do?

  • A) Runs if an exception IS raised
  • B) Runs if no exception is raised in the try block
  • C) Runs regardless of whether an exception occurred
  • D) Runs only if a specific exception type is raised
Answer **B)** Runs if no exception is raised in the `try` block *Why B:* The `else` block runs only when the `try` block completes successfully — no exceptions raised. *Why not A:* That's what `except` does. *Why not C:* That's what `finally` does. *Reference:* Section 11.6.1

5. What is the output of this code?

try:
    x = int("42")
except ValueError:
    print("error")
else:
    print("success")
finally:
    print("done")
  • A) error then done
  • B) success then done
  • C) done only
  • D) success only
Answer **B)** `success` then `done` *Why B:* `int("42")` succeeds (no exception), so the `except` block is skipped. The `else` block runs because no exception occurred, printing "success". The `finally` block always runs, printing "done". *Reference:* Section 11.6

6. Which of the following is a reason to use finally?

  • A) To handle specific exception types
  • B) To ensure cleanup code runs regardless of whether an exception occurred
  • C) To raise custom exceptions
  • D) To convert syntax errors to runtime errors
Answer **B)** To ensure cleanup code runs regardless of whether an exception occurred *Why B:* `finally` is guaranteed to execute whether the `try` block succeeds, an exception is caught, or an exception propagates. This makes it ideal for cleanup (closing files, releasing resources). *Reference:* Section 11.6.2

7. What does EAFP stand for?

  • A) Error And Fault Protection
  • B) Easier to Ask Forgiveness than Permission
  • C) Exception Anticipation For Programs
  • D) Early And Fast Processing
Answer **B)** Easier to Ask Forgiveness than Permission *Why B:* EAFP is Python's preferred philosophy — try the operation first, then handle the exception if it fails. This contrasts with LBYL (Look Before You Leap), which checks conditions before attempting the operation. *Reference:* Section 11.8

8. Which approach is generally considered more "Pythonic"?

  • A) LBYL — always check conditions before attempting an operation
  • B) EAFP — attempt the operation and handle exceptions if it fails
  • C) Both are equally Pythonic
  • D) Neither — Python has no preference
Answer **B)** EAFP — attempt the operation and handle exceptions if it fails *Why B:* Python's culture and design strongly favor EAFP. It keeps the happy path clear, avoids race conditions, and is endorsed by Python core developers. The Zen of Python and official Python docs both reflect this preference. *Reference:* Section 11.8

9. Why is a bare except: (without specifying an exception type) considered an anti-pattern?

  • A) It's a syntax error in Python 3.12+
  • B) It catches too many exceptions, including KeyboardInterrupt and SystemExit
  • C) It's slower than specifying exception types
  • D) It only catches ValueError
Answer **B)** It catches too many exceptions, including `KeyboardInterrupt` and `SystemExit` *Why B:* A bare `except:` catches everything, including `KeyboardInterrupt` (Ctrl+C) and `SystemExit`. This makes the program hard to stop and hides real bugs. Always specify the exception type you expect. *Reference:* Section 11.10.1

10. What is the purpose of raise in Python?

  • A) To catch an exception
  • B) To suppress an exception
  • C) To deliberately trigger an exception
  • D) To convert an exception to a warning
Answer **C)** To deliberately trigger an exception *Why C:* `raise` creates and throws an exception object. It's used in functions to signal that something went wrong — for example, `raise ValueError("Score must be 0-100")`. The caller is responsible for catching it. *Reference:* Section 11.7

Section 2: Short Answer (2 points each)

11. What is the difference between a ValueError and a TypeError? Give an example of each.

Answer `ValueError` means the value is the right type but wrong content: `int("hello")` — a string is valid for `int()`, but `"hello"` isn't a valid integer string. `TypeError` means the type itself is wrong for the operation: `len(42)` — `len()` expects a sequence/collection, but an integer isn't one. *Reference:* Section 11.2.2

12. Explain the difference between the else and finally clauses in a try statement.

Answer `else` runs only when the `try` block completes successfully (no exception raised). It's for code that should execute on success but shouldn't be inside `try` (to avoid accidentally catching its exceptions). `finally` runs no matter what — whether the `try` succeeded, an exception was caught, or an exception is propagating uncaught. It's for cleanup that must always happen (closing files, releasing resources). *Reference:* Section 11.6

13. What is the output of this code?

try:
    result = 10 / 0
except ZeroDivisionError:
    print("A")
else:
    print("B")
finally:
    print("C")
print("D")
Answer
A
C
D
`10 / 0` raises `ZeroDivisionError`, so the `except` block runs (prints "A"). The `else` block is skipped (it only runs on success). `finally` always runs (prints "C"). The exception was caught, so the program continues and prints "D". *Reference:* Section 11.6.3

14. Convert this LBYL code to EAFP style:

import os
if os.path.exists("config.txt"):
    with open("config.txt") as f:
        config = f.read()
else:
    config = "default settings"
Answer
try:
    with open("config.txt") as f:
        config = f.read()
except FileNotFoundError:
    config = "default settings"
The EAFP version eliminates the `os.path.exists()` check and handles the actual exception. It also avoids the race condition where the file could be deleted between the check and the `open()` call. *Reference:* Section 11.8

15. Why should error messages in raise statements include the actual problematic value?

Answer Including the actual value helps the developer (or user) diagnose the problem quickly. Compare: - Bad: `raise ValueError("invalid score")` — doesn't tell you what the score was - Good: `raise ValueError(f"Score must be 0-100, got {score}")` — shows exactly what went wrong Without the actual value, you have to add print statements or use a debugger to figure out what input caused the error. *Reference:* Section 11.7.2

Section 3: Code Analysis (3 points each)

16. This code has an error handling anti-pattern. Identify it and explain why it's problematic:

def load_data(filename):
    try:
        with open(filename) as f:
            data = json.loads(f.read())
        processed = transform_data(data)
        save_results(processed)
    except Exception:
        print("Something went wrong.")
Answer **Two anti-patterns:** 1. **Catching too broadly (`Exception`):** If `transform_data` or `save_results` has a bug (e.g., `NameError`, `AttributeError`), this catches it and prints a vague message. The developer won't know whether the problem is in file I/O, JSON parsing, data transformation, or saving. 2. **The `try` block is too large:** It wraps three separate operations. Each could fail for different reasons that require different responses. **Better approach:** Use separate `try`/`except` blocks (or a narrower one) with specific exception types:
def load_data(filename):
    try:
        with open(filename) as f:
            data = json.loads(f.read())
    except FileNotFoundError:
        print(f"File '{filename}' not found.")
        return
    except json.JSONDecodeError:
        print(f"File '{filename}' contains invalid JSON.")
        return

    processed = transform_data(data)
    save_results(processed)
*Reference:* Sections 11.10.2, 11.6.1

17. Trace through this code and determine its output:

def risky(n):
    try:
        if n == 0:
            raise ValueError("zero!")
        result = 100 / n
    except ValueError as e:
        print(f"Caught ValueError: {e}")
        return -1
    except ZeroDivisionError:
        print("Caught ZeroDivisionError")
        return -2
    else:
        print(f"Success: {result}")
        return result
    finally:
        print("Cleanup")

x = risky(0)
print(f"x = {x}")
Answer
Caught ValueError: zero!
Cleanup
x = -1
`n == 0` is True, so `raise ValueError("zero!")` executes. The `except ValueError` block catches it and prints `"Caught ValueError: zero!"`. The `return -1` is queued. Before the function actually returns, `finally` runs and prints `"Cleanup"`. Then the function returns `-1`, and `x = -1` is printed. Note: `ZeroDivisionError` is never reached because the `raise ValueError` happens before `100 / n`. *Reference:* Section 11.6.3

18. What's wrong with this custom exception and how would you fix it?

class NegativeBalanceError:
    def __init__(self, balance):
        self.balance = balance
        self.message = f"Balance cannot be negative: {balance}"
Answer `NegativeBalanceError` doesn't inherit from `Exception` (or any exception class). It's just a regular class, so Python won't let you use it with `raise`:
raise NegativeBalanceError(-50)
# TypeError: exceptions must derive from BaseException
**Fix:** Inherit from `Exception` and call `super().__init__()`:
class NegativeBalanceError(Exception):
    def __init__(self, balance):
        self.balance = balance
        super().__init__(f"Balance cannot be negative: {balance}")
*Reference:* Section 11.9

Section 4: Scenario Analysis (3 points each)

19. You're writing a web scraper that downloads 1,000 pages. About 5% of the URLs will return 404 errors. Should you use LBYL (check if the URL is valid before downloading) or EAFP (try to download and handle the error)? Explain your reasoning.

Answer **EAFP is the better choice** for several reasons: 1. **Race condition:** A URL could become invalid between the check and the download. There's no reliable way to "check" if a URL will work without actually requesting it. 2. **Network overhead:** An LBYL approach (e.g., a HEAD request first) doubles the number of HTTP requests. With EAFP, you make one request and handle the 404 if it occurs. 3. **5% error rate is low:** Exceptions in 5% of cases means the happy path dominates. EAFP is most efficient when exceptions are rare. 4. **Multiple failure modes:** URLs can fail for many reasons (404, timeout, DNS failure, connection reset). EAFP naturally handles all of them. *Reference:* Section 11.8

20. Elena's report script processes 500 CSV rows. About 200 have data quality issues (missing fields, non-numeric values). She wants to process all valid rows and log the bad ones. Design the error handling approach. Should she use LBYL or EAFP? Why? What exception types should she catch?

Answer With 200 out of 500 rows (40%) being problematic, this is a case where **a hybrid approach** is best: - **LBYL for predictable validation:** Check for empty rows, wrong number of fields, and obviously invalid data before processing. These are expected and common, so `if` checks are clearer and more efficient than exception handling for 40% of the data. - **EAFP for unpredictable errors:** Use `try`/`except` around type conversions (`int()`, `float()`) and other operations that might fail in unexpected ways. **Exception types to catch:** - `ValueError` — for failed numeric conversions - `IndexError` — for rows with fewer fields than expected **Pattern:**
valid_rows = []
errors = []
for i, line in enumerate(lines):
    try:
        fields = line.strip().split(",")
        if len(fields) != 3:  # LBYL — predictable check
            errors.append((i, "wrong number of fields"))
            continue
        name, score, grade = fields
        score = float(score)  # EAFP — let ValueError happen
        valid_rows.append((name, score, grade))
    except ValueError as e:
        errors.append((i, str(e)))
*Reference:* Sections 11.8.3, 11.8.4