Exception Handling

15 questions found

How does a try/except block work, and what happens if no matching except clause exists?

Beginner
Code in 'try' runs normally; if it raises an exception, Python looks for the FIRST 'except' clause matching that exception's type (checked top to bottom) and runs it — if none match, the exception propagates up the call stack, potentially crashing the program if never caught anywhere.
try:
    result = 10 / 0
except ZeroDivisionError:
    print("Cannot divide by zero")
except ValueError:
    print("Invalid value")
Real-world example Gracefully handling a division-by-zero or invalid-input error instead of letting the program crash with an unhandled traceback.

Common follow-ups: What happens if you put a more GENERAL exception type's except clause BEFORE a more SPECIFIC one?

Data Types & Structures

What does the 'finally' block guarantee, and when is it typically used?

Beginner
Code in 'finally' ALWAYS runs after try (and any except) completes, whether an exception occurred or not, and even if the try block contains a 'return' — making it the standard place for cleanup code like closing a file or releasing a lock.
file = open("data.txt")
try:
    process(file)
finally:
    file.close()  # always runs, guaranteeing the file is closed
Real-world example Guaranteeing a file handle or network connection is properly closed regardless of whether processing succeeded or raised.

Common follow-ups: How does the 'with' statement relate to and often replace a manual try/finally for resource cleanup?

File I/O & Context Managers

How do you catch multiple exception types with a single except clause?

Beginner
List the exception types as a TUPLE after 'except', separated by commas — the clause catches ANY of the listed types, running the same handling code regardless of which specific one was raised.
try:
    value = int(user_input)
    result = 10 / value
except (ValueError, ZeroDivisionError) as e:
    print(f"Invalid input: {e}")
Real-world example Handling several related error conditions (like invalid input format AND division by zero) with the same recovery logic.

Common follow-ups: How would you handle these SAME exception types differently, with SEPARATE recovery logic for each?

Exception Handling

How do you create a custom exception class, and what's the minimal amount of code needed?

Intermediate
Define a class inheriting from Exception (or a more specific built-in exception), typically with no additional code needed at all (just 'pass') unless you want to add custom attributes or override __init__ — Python's exception machinery handles the rest automatically via inheritance.
class InsufficientFundsError(Exception):
    def __init__(self, shortfall):
        super().__init__(f"Insufficient funds. Short by ${shortfall:.2f}")
        self.shortfall = shortfall

try:
    raise InsufficientFundsError(50.00)
except InsufficientFundsError as e:
    print(e.shortfall)  # 50.0
Real-world example Raising a domain-specific exception (like InsufficientFundsError) that carries meaningful business context beyond a generic message.

Common follow-ups: Why is it good practice to inherit custom exceptions from a specific built-in type rather than always using the bare 'Exception'?

OOP

What is the difference between 'raise' and 'raise ... from ...' for exception chaining?

Intermediate
Plain 'raise NewException()' inside an except block still shows BOTH the original and new exception in the traceback (implicit chaining, labeled 'During handling of the above exception...'); 'raise NewException() from original_exception' makes the chaining EXPLICIT, and 'raise NewException() from None' SUPPRESSES the original exception's context entirely, showing only the new one.
try:
    int("not a number")
except ValueError as e:
    raise RuntimeError("Failed to parse config") from e  # explicit chaining: shows both, labeled 'direct cause'

try:
    int("not a number")
except ValueError:
    raise RuntimeError("Failed to parse config") from None  # suppresses the ValueError entirely
Real-world example Wrapping a low-level parsing error in a more meaningful, higher-level exception while preserving (or intentionally hiding) the original cause.

Common follow-ups: When would you deliberately want to suppress the original exception's context using 'from None'?

Debugging & Profiling

How do you access details about the exception currently being handled, like its type, value, and traceback, using sys.exc_info()?

Intermediate
sys.exc_info() returns a 3-tuple (exception type, exception instance, traceback object) for the exception CURRENTLY being handled — mostly superseded by simply using 'except Exception as e' to directly capture the instance, but still useful for lower-level introspection or re-raising logic.
import sys

try:
    1 / 0
except ZeroDivisionError:
    exc_type, exc_value, exc_tb = sys.exc_info()
    print(exc_type)   # <class 'ZeroDivisionError'>
    print(exc_value)  # division by zero
Real-world example Building generic exception-logging or reporting middleware that needs low-level access to full exception details.

Common follow-ups: Why is 'except Exception as e' generally preferred over manually calling sys.exc_info() in modern code?

Debugging & Profiling

How does the 'else' clause on a try statement work, and what's its purpose compared to just putting that code at the end of 'try'?

Intermediate
The 'else' clause runs ONLY if the 'try' block completed WITHOUT raising any exception — putting code there (instead of at the end of 'try') makes it explicit that this code is NOT meant to be protected by the except clauses, avoiding accidentally catching exceptions from code that wasn't the original risky operation.
try:
    value = risky_parse(data)
except ValueError:
    print("Parsing failed")
else:
    print(f"Parsed successfully: {value}")  # only runs if risky_parse succeeded, and its own errors aren't caught above
Real-world example Clearly separating the 'risky' operation being protected from subsequent code that shouldn't accidentally have its own errors caught by the same except clause.

Common follow-ups: What would go wrong if the 'else' block's code were instead placed at the END of the 'try' block?

Debugging & Profiling

How does Python 3.11's exception groups (ExceptionGroup) and the 'except*' syntax let you handle MULTIPLE unrelated exceptions raised together?

Advanced
ExceptionGroup wraps multiple exceptions raised together (common in concurrent code, like asyncio.gather with multiple failures) as a single object; 'except*' matches and handles the SUBSET of exceptions within the group matching a given type, potentially re-raising a NEW ExceptionGroup with the remaining unmatched ones.
try:
    raise ExceptionGroup("multiple failures", [ValueError("bad value"), TypeError("bad type")])
except* ValueError as eg:
    print(f"Handled ValueErrors: {eg.exceptions}")
except* TypeError as eg:
    print(f"Handled TypeErrors: {eg.exceptions}")
Real-world example Handling multiple independent failures from concurrent tasks (like several parallel API calls) that fail simultaneously with different exception types.

Common follow-ups: How does asyncio.gather() (or TaskGroup in 3.11+) actually produce an ExceptionGroup from multiple failed concurrent tasks?

Concurrency (asyncio/threading/multiprocessing)

How would you design a custom exception HIERARCHY for an application, and why does this matter for how callers can catch errors at different granularities?

Advanced
Create a base application exception (e.g., AppError), then specific subclasses for different error categories (e.g., ValidationError, NotFoundError, all inheriting from AppError) — callers can catch broadly (`except AppError`) to handle ALL application errors generically, or narrowly (`except ValidationError`) for specific recovery logic, without needing to know every specific subclass upfront.
class AppError(Exception):
    pass

class ValidationError(AppError):
    pass

class NotFoundError(AppError):
    pass

try:
    validate_and_fetch(data)
except ValidationError:
    return {"error": "invalid input"}, 400
except AppError:
    return {"error": "application error"}, 500  # catches NotFoundError and any FUTURE AppError subclass too
Real-world example Designing a web API's error-handling layer so new exception types added later are automatically caught by broader existing handlers.

Common follow-ups: Why is designing a THOUGHTFUL exception hierarchy upfront valuable for a growing codebase's error handling?

OOP

How does context manager __exit__ interact with exception handling, and how can it SUPPRESS an exception by returning True?

Advanced
__exit__(self, exc_type, exc_value, traceback) receives details of any exception raised inside the 'with' block; if it returns a TRUTHY value, the exception is SUPPRESSED entirely (treated as handled, execution continues after the 'with' block) — returning None/False (the default) lets the exception propagate normally.
class SuppressErrors:
    def __enter__(self):
        return self
    def __exit__(self, exc_type, exc_value, tb):
        if exc_type is ValueError:
            print(f"Suppressed: {exc_value}")
            return True  # suppresses the ValueError
        return False  # lets other exception types propagate normally

with SuppressErrors():
    raise ValueError("this gets suppressed")
print("Execution continues here")
Real-world example Building a custom context manager (similar to contextlib.suppress) that intentionally swallows specific, expected exception types.

Common follow-ups: How does the standard library's contextlib.suppress() context manager provide this exact same capability more concisely?

File I/O & Context Managers

Showing 1–10 of 15