Python Exception Handling
Python Exception Handling Interview Questions
# Exception (can be caught) try: x = 1 / 0 # ZeroDivisionError except ZeroDivisionError: print("Cannot divide by zero")
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")
# try-finally: For cleanup (file close, resource release) file = None try: file = open("data.txt") process(file) finally: if file: file.close() # Always executed
# 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
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}")
divide(10, 2) # Output: "Division successful: 5.0", "Cleanup" divide(10, 0) # Output: "Cannot divide by zero", "Cleanup"
# 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!
# 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")
# 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
During handling of the above exception, another exception occurred...
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
# 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
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())
# 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)
# 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!
# 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
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
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")
# 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)
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.
Tricky interview questions
Yes. Python runs finally before the function actually returns, so cleanup there always executes unless the process dies first.
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.
It catches BaseException, including KeyboardInterrupt and SystemExit, so it can mask interpreter control flow and make shutdown/debugging painful.
Usually no — KeyboardInterrupt subclasses BaseException, not Exception, so users can still interrupt unless you catch too broadly.
It re-raises the active exception while preserving the traceback — preferred over losing context when you only log then propagate.
from chains exceptions (__cause__) for debugging. from None deliberately hides the original chain when wrapping — use sparingly and document why.
The exception raised in the block is suppressed at that point — powerful but dangerous unless suppression is intentional and documented.
Plain string wrapping drops traceback linkage unless you chain — interviewers look for awareness of lost stack traces and PEP 3134 chaining.
No — python -O strips asserts. Treat validation as real control flow: raise ValueError / custom errors with clear messages.
The first matching handler wins. Put specific subclasses (e.g. FileNotFoundError) before broad OSError or Exception so handlers are reachable.
Raising StopIteration inside a generator now maps to RuntimeError — accidental stops became silent bugs before the change.
It matches parts of an ExceptionGroup — handy when concurrent tasks surface bundled failures instead of one-at-a-time semantics.
It subclasses BaseException (since 3.8+) so blind except Exception patterns may mishandle cancellation vs prior semantics — interviews test cancellation hygiene.
The thread terminates; the error does not automatically crash the main thread — configure threading.excepthook / logging so failures are visible.
Yes — finally runs before leaving the try suite for those constructs; contrast with loop else which interacts differently with break.
Generators receive GeneratorExit on close — swallowing or raising wrong errors can mask shutdown and resource bugs.
Concrete subclasses encode intent and errno plumbing — callers handle missing paths without parsing strings.
logging.exception must be called from an except block — it captures the traceback automatically; both techniques aid observability.
Tracebacks reference frames/locals — holding exceptions (especially in lists/singletons) can delay GC of large objects until the traceback is dropped.
Top-level services translating unknown failures into HTTP 500 / RPC errors — still rethrow typed domain errors inside libraries so callers can branch.