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")
# 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")
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
# 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
# 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}")
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"
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!
# 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")
# 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
# 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...
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
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
# 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())
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)
# 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!
# 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
# 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
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")
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)
# 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.
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.