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
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.
Python Exception Handling Next