Python Exception Handling

Python Exception Handling Interview Questions

What is the difference between syntax error and exception in Python?
Syntax Error:
Occurs when Python cannot parse the code (incorrect syntax). Cannot be handled with try-except. Must be fixed.
Exception:
Occurs during execution (runtime error). Can be handled with try-except.
# Syntax Error (cannot be caught) # print("Hello) # Missing closing quote
# Exception (can be caught) try:     x = 1 / 0 # ZeroDivisionError except ZeroDivisionError:     print("Cannot divide by zero")
What are the basic components of exception handling in Python?
Python exception handling uses:
try:     # Code that might raise exception     result = 10 / 0
except ZeroDivisionError as e:     # Handle specific exception     print(f"Error: {e}")
except (TypeError, ValueError) as e:     # Handle multiple exceptions     print(f"Type or Value error: {e}")
except Exception as e:     # Handle any exception     print(f"General error: {e}")
else:     # Execute if no exception     print("No errors!")
finally:     # Always execute     print("Cleanup code")
What is the difference between try-except and try-finally?
try-except:
Catches and handles exceptions.
try-finally:
Executes cleanup code regardless of exception (no catching).
# try-except: For error handling try:     risky_operation() except ValueError:     handle_error()
# try-finally: For cleanup (file close, resource release) file = None try:     file = open("data.txt")     process(file) finally:     if file:         file.close() # Always executed
How to raise exceptions manually in Python?
Use raise statement to raise exceptions:
# Raise built-in exception def validate_age(age):     if age < 0:         raise ValueError("Age cannot be negative")     if age > 150:         raise ValueError("Age is unrealistic")
# Re-raise exception try:     validate_age(-5) except ValueError as e:     print(f"Caught: {e}")     raise # Re-raise the same exception
# Raise with custom message raise RuntimeError("Something went wrong") from None
How to create custom exceptions in Python?
Create a class that inherits from Exception or its subclasses:
class InvalidEmailError(Exception):     """Exception raised for invalid email format."""     def __init__(self, email, message="Invalid email format"):         self.email = email         self.message = message         super().__init__(f"{message}: {email}")
class EmptyInputError(Exception):     pass # Simple custom exception
# Using custom exceptions def validate_email(email):     if "@" not in email:         raise InvalidEmailError(email)     if not email:         raise EmptyInputError("Email cannot be empty")
try:     validate_email("invalid-email") except InvalidEmailError as e:     print(f"Custom error: {e}")
What is the exception hierarchy in Python?
All exceptions inherit from BaseException. Key hierarchy:
BaseException ├── SystemExit ├── KeyboardInterrupt ├── GeneratorExit └── Exception     ├── ArithmeticError     │    ├── ZeroDivisionError     │    └── FloatingPointError     ├── LookupError     │    ├── IndexError     │    └── KeyError     ├── OSError     │    ├── FileNotFoundError     │    └── PermissionError     ├── TypeError     ├── ValueError     └── RuntimeError
Best Practice: Catch specific exceptions before general ones. Don't catch BaseException unless necessary (catches SystemExit, KeyboardInterrupt).
What is the purpose of the else clause in try-except?
else executes only if no exceptions were raised in the try block. Useful for code that should run only when try succeeds.
def divide(a, b):     try:         result = a / b     except ZeroDivisionError:         print("Cannot divide by zero")     else:         # Only executes if no exception         print(f"Division successful: {result}")         return result     finally:         print("Cleanup")
divide(10, 2) # Output: "Division successful: 5.0", "Cleanup" divide(10, 0) # Output: "Cannot divide by zero", "Cleanup"
Use Case: Put code in else that shouldn't be protected by try-except but should run only if try succeeds.
What is the difference between except Exception and bare except?
except Exception:
Catches all regular exceptions (recommended).
except:
Bare except - catches ALL exceptions including SystemExit, KeyboardInterrupt (dangerous!).
# GOOD: Catches regular exceptions try:     risky_code() except Exception as e: # Recommended     handle_error(e)
# BAD: Catches EVERYTHING (including Ctrl+C) try:     risky_code() except: # Avoid! Catches SystemExit, KeyboardInterrupt     handle_error()
# BAD: Even worse - silent fail try:     risky_code() except:     pass # Never do this!
Warning: Never use bare except! It can make debugging impossible and prevent program termination.
How to handle multiple exceptions in Python?
Multiple approaches:
# Method 1: Multiple except blocks try:     risky_code() except ValueError:     handle_value_error() except TypeError:     handle_type_error() except (IndexError, KeyError):     handle_lookup_error()
# Method 2: Single except with tuple try:     risky_code() except (ValueError, TypeError, KeyError) as e:     print(f"Caught error: {type(e).__name__}: {e}")
# Method 3: Using Exception groups (Python 3.11+) try:     risky_code() except* ValueError:     print("Handle ValueError") except* TypeError:     print("Handle TypeError")
What is exception chaining in Python?
Exception chaining shows the relationship between exceptions. Use raise ... from.
# Explicit chaining try:     x = int("not-a-number") except ValueError as e:     raise RuntimeError("Conversion failed") from e
# Implicit chaining def process_data(data):     try:         return int(data)     except ValueError:         raise RuntimeError("Processing failed")
# Disable chaining try:     risky_code() except ValueError:     raise RuntimeError("New error") from None
Traceback shows:
During handling of the above exception, another exception occurred...
How to create a context manager for exception handling?
Create context manager using class with __enter__ and __exit__ methods:
class DatabaseConnection:     def __init__(self, db_name):         self.db_name = db_name         self.connection = None
    def __enter__(self):         print(f"Connecting to {self.db_name}")         self.connection = "connected" # Simulate connection         return self
    def __exit__(self, exc_type, exc_val, exc_tb):         print(f"Closing connection to {self.db_name}")         self.connection = None         # Return True to suppress exception, False to propagate         return False
# Usage with DatabaseConnection("mydb") as db:     print(f"Using connection: {db.connection}")     # Connection automatically closed even if exception occurs
What are assertion errors and when to use them?
assert statements raise AssertionError if condition is False. Use for debugging and sanity checks.
# Basic assertion def calculate_area(width, height):     assert width > 0, "Width must be positive"     assert height > 0, "Height must be positive"     return width * height
# Type checking def process_data(data):     assert isinstance(data, (list, tuple)), "Data must be list or tuple"     assert len(data) > 0, "Data cannot be empty"
# Disable assertions (run with -O flag) # python -O script.py # Disables all assertions
Important: Assertions can be disabled with -O flag. Don't use them for data validation in production! Use proper exception handling instead.
How to get exception information programmatically?
Use sys.exc_info() or exception object attributes:
import sys import traceback
try:     x = 1 / 0 except ZeroDivisionError as e:     # Method 1: sys.exc_info()     exc_type, exc_value, exc_traceback = sys.exc_info()     print(f"Type: {exc_type.__name__}") # ZeroDivisionError     print(f"Value: {exc_value}") # division by zero
    # Method 2: Exception attributes     print(f"Args: {e.args}") # ('division by zero',)     print(f"Message: {str(e)}") # division by zero
    # Method 3: Traceback     tb_lines = traceback.format_exception(exc_type, exc_value, exc_traceback)     for line in tb_lines:         print(line.strip())
What is the warnings module and how is it different from exceptions?
warnings module shows non-fatal issues. Exceptions stop execution, warnings don't (by default).
import warnings
# Simple warning warnings.warn("This feature is deprecated", DeprecationWarning)
# Warning with stacklevel def deprecated_func():     warnings.warn(         "deprecated_func is deprecated",         DeprecationWarning,         stacklevel=2 # Show caller in warning     )
# Convert warnings to exceptions warnings.filterwarnings("error", category=DeprecationWarning) # Now DeprecationWarning raises exception
# Filter warnings warnings.filterwarnings("ignore", category=UserWarning)
Common warning categories: DeprecationWarning, FutureWarning, UserWarning, RuntimeWarning, SyntaxWarning
How to handle KeyboardInterrupt and SystemExit exceptions?
These inherit from BaseException (not Exception). Handle them separately or let them propagate.
import sys import time
# Proper handling try:     while True:         time.sleep(1)         print("Working...") except KeyboardInterrupt:     print("\nInterrupted by user")     sys.exit(0) # Clean exit
# SystemExit handling try:     sys.exit("Exiting program") except SystemExit as e:     print(f"Exit code: {e.code}")     raise # Re-raise to actually exit
# BAD: This catches everything including Ctrl+C try:     infinite_loop() except: # Catches KeyboardInterrupt!     pass # Can't interrupt with Ctrl+C!
What is the suppress context manager for exceptions?
contextlib.suppress suppresses specified exceptions within a context.
from contextlib import suppress import os
# Instead of try-except-pass with suppress(FileNotFoundError):     os.remove("tempfile.txt") # FileNotFoundError is silently ignored
# Multiple exceptions with suppress(KeyError, IndexError):     value = my_dict["missing_key"]     item = my_list[999]
# Equivalent traditional code (more verbose) try:     os.remove("tempfile.txt") except FileNotFoundError:     pass
Use Case: When you expect an exception and want to ignore it cleanly. Better than try-except-pass.
How to implement retry logic with exception handling?
Implement retry with loop and exception handling:
import time from requests.exceptions import RequestException
def retry_operation(max_retries=3, delay=1):     for attempt in range(max_retries):         try:             return risky_operation()         except (ConnectionError, TimeoutError) as e:             if attempt == max_retries - 1:                 raise # Re-raise on last attempt             print(f"Attempt {attempt + 1} failed: {e}. Retrying...")             time.sleep(delay)         else:             # Other exceptions - don't retry             raise
# With exponential backoff def retry_with_backoff(max_retries=5, initial_delay=1, backoff_factor=2):     delay = initial_delay     for attempt in range(max_retries):         try:             return network_call()         except RequestException:             if attempt == max_retries - 1:                 raise             time.sleep(delay)             delay *= backoff_factor
What are exception groups in Python 3.11+?
Exception groups allow raising/handling multiple exceptions simultaneously. Use except*.
# Raising exception group def validate_user(user_data):     errors = []     if "name" not in user_data:         errors.append(ValueError("Name is required"))     if "email" not in user_data:         errors.append(ValueError("Email is required"))     if "age" in user_data and user_data["age"] < 0:         errors.append(ValueError("Age cannot be negative"))
    if errors:         raise ExceptionGroup("Validation failed", errors)
# Handling with except* try:     validate_user({"age": -5}) except* ValueError as eg:     for error in eg.exceptions:         print(f"Validation error: {error}")
# Multiple except* blocks try:     complex_operation() except* ValueError:     print("Handle ValueError") except* TypeError:     print("Handle TypeError")
How to log exceptions properly?
Use logging module with appropriate levels:
import logging import traceback
# Configure logging logging.basicConfig(     level=logging.ERROR,     format='%(asctime)s - %(name)s - %(levelname)s - %(message)s' )
# Log exception with traceback try:     risky_operation() except Exception as e:     logging.error(f"Operation failed: {e}", exc_info=True)     # OR     logging.exception("Operation failed") # Automatically includes traceback
# Structured logging try:     process_data(data) except (ValueError, TypeError) as e:     logging.warning(         "Data processing warning",         extra={"data": data, "error": str(e)}     )
# Log to different handlers logger = logging.getLogger(__name__) file_handler = logging.FileHandler("errors.log") logger.addHandler(file_handler)
What are best practices for exception handling in Python?
1. Be Specific: Catch specific exceptions, not general Exception unless necessary.
2. Don't Suppress: Never use bare except or except: pass.
3. Log Properly: Always log exceptions with context.
4. Clean Resources: Use finally or context managers for cleanup.
5. Preserve Traceback: Use raise ... from for chaining.
6. Create Meaningful Errors: Custom exceptions with clear messages.
7. Don't Overuse: Exceptions for exceptional cases, not control flow.
8. Test Exception Paths: Test both happy and exception paths.
# GOOD example def process_file(filepath):     try:         with open(filepath, 'r') as f:             return f.read()     except FileNotFoundError:         logging.error(f"File not found: {filepath}")         raise FileNotFoundError(f"Could not find {filepath}")     except PermissionError:         logging.error(f"Permission denied: {filepath}")         raise     except OSError as e:         logging.error(f"OS error: {e}")         raise RuntimeError(f"Failed to read {filepath}") from e

Tricky interview questions

Does finally run when try executes a return?

Yes. Python runs finally before the function actually returns, so cleanup there always executes unless the process dies first.

When does a try / except / else clause run?

else runs only if the try suite finishes without raising an exception. If anything raises (even if caught later in outer frames), that else is skipped.

Why is bare except: considered harmful?

It catches BaseException, including KeyboardInterrupt and SystemExit, so it can mask interpreter control flow and make shutdown/debugging painful.

Will except Exception: catch Ctrl+C?

Usually no — KeyboardInterrupt subclasses BaseException, not Exception, so users can still interrupt unless you catch too broadly.

What does bare raise inside except do?

It re-raises the active exception while preserving the traceback — preferred over losing context when you only log then propagate.

Explain raise NewError(...) from original_exc vs from None.

from chains exceptions (__cause__) for debugging. from None deliberately hides the original chain when wrapping — use sparingly and document why.

If a context manager’s __exit__ returns True, what happens?

The exception raised in the block is suppressed at that point — powerful but dangerous unless suppression is intentional and documented.

Why is raise SomeError(str(e)) often worse than raise ... from e?

Plain string wrapping drops traceback linkage unless you chain — interviewers look for awareness of lost stack traces and PEP 3134 chaining.

Are assert statements suitable for validating user input?

No — python -O strips asserts. Treat validation as real control flow: raise ValueError / custom errors with clear messages.

Order of multiple except clauses — why does it matter?

The first matching handler wins. Put specific subclasses (e.g. FileNotFoundError) before broad OSError or Exception so handlers are reachable.

What did PEP 479 change about StopIteration in generators?

Raising StopIteration inside a generator now maps to RuntimeError — accidental stops became silent bugs before the change.

What is except* for (Python 3.11+)?

It matches parts of an ExceptionGroup — handy when concurrent tasks surface bundled failures instead of one-at-a-time semantics.

Why is asyncio.CancelledError tricky to catch?

It subclasses BaseException (since 3.8+) so blind except Exception patterns may mishandle cancellation vs prior semantics — interviews test cancellation hygiene.

What happens to an uncaught exception in a worker thread?

The thread terminates; the error does not automatically crash the main thread — configure threading.excepthook / logging so failures are visible.

Does finally run when a loop inside try executes break or continue?

Yes — finally runs before leaving the try suite for those constructs; contrast with loop else which interacts differently with break.

Why mishandle GeneratorExit?

Generators receive GeneratorExit on close — swallowing or raising wrong errors can mask shutdown and resource bugs.

Why prefer FileNotFoundError over plain OSError when possible?

Concrete subclasses encode intent and errno plumbing — callers handle missing paths without parsing strings.

logging.exception vs logging.error(..., exc_info=True)?

logging.exception must be called from an except block — it captures the traceback automatically; both techniques aid observability.

Can storing exceptions long-lived cause subtle memory issues?

Tracebacks reference frames/locals — holding exceptions (especially in lists/singletons) can delay GC of large objects until the traceback is dropped.

Where is a broad “catch-all then log” acceptable at API boundaries?

Top-level services translating unknown failures into HTTP 500 / RPC errors — still rethrow typed domain errors inside libraries so callers can branch.

Note: Exception handling is crucial for robust Python applications. Remember: try-except for catching, raise for throwing, finally for cleanup. Always catch specific exceptions, log errors properly, and use context managers for resource management. Python 3.11+ adds powerful features like exception groups for handling multiple errors.
Prev: Static Python Exception Handling Next: Regex