πŸŽ‰ 75% of content is free forever β€” Unlock Premium from $10/mo β†’
CW
Search courses…
πŸ’Ό Servicesℹ️ Aboutβœ‰οΈ ContactView Pricing Plansfrom $10

Exception Handling: EAFP, Context Managers, Custom Exceptions

PythonError Handling⭐ Premium

Advertisement

Google, Meta & Amazon Interview

Exception Handling: EAFP, Context Managers, Custom Exceptions

Pythonic error handling patterns and best practices

Interview Question

"Explain Python's exception handling philosophy. What is EAFP? How do you create custom exceptions? When should you use context managers for error handling?"

Difficulty: Medium | Frequently asked at Google, Meta, Amazon


Theoretical Foundation

EAFP vs LBYL

# LBYL: Look Before You Leap
# Check condition before acting
if key in dictionary:
    value = dictionary[key]
else:
    value = default_value

# Easier to Ask for Forgiveness than Permission
# Try action, handle exception
try:
    value = dictionary[key]
except KeyError:
    value = default_value

# EAFP is more Pythonic because:
# 1. Less code duplication
# 2. Handles race conditions
# 3. Follows Python's duck typing philosophy

ℹ️

Key Concept: Python favors EAFP (try/except) over LBYL (if checks) for cleaner, more Pythonic code.


Exception Hierarchy

# Python exception hierarchy
"""
BaseException
β”œβ”€β”€ SystemExit
β”œβ”€β”€ KeyboardInterrupt
β”œβ”€β”€ GeneratorExit
└── Exception
    β”œβ”€β”€ StopIteration
    β”œβ”€β”€ ArithmeticError
    β”‚   β”œβ”€β”€ FloatingPointError
    β”‚   β”œβ”€β”€ OverflowError
    β”‚   └── ZeroDivisionError
    β”œβ”€β”€ AttributeError
    β”œβ”€β”€ EOFError
    β”œβ”€β”€ ImportError
    β”‚   └── ModuleNotFoundError
    β”œβ”€β”€ LookupError
    β”‚   β”œβ”€β”€ IndexError
    β”‚   └── KeyError
    β”œβ”€β”€ NameError
    β”‚   └── UnboundLocalError
    β”œβ”€β”€ OSError
    β”‚   β”œβ”€β”€ FileNotFoundError
    β”‚   β”œβ”€β”€ FileExistsError
    β”‚   └── PermissionError
    β”œβ”€β”€ RuntimeError
    β”‚   β”œβ”€β”€ NotImplementedError
    β”‚   └── RecursionError
    β”œβ”€β”€ SyntaxError
    β”‚   └── IndentationError
    β”œβ”€β”€ TypeError
    β”œβ”€β”€ ValueError
    β”‚   └── UnicodeError
    └── Warning
"""

# Inspect exception hierarchy
def print_hierarchy(cls, indent=0):
    print(" " * indent + cls.__name__)
    for subclass in cls.__subclasses__():
        print_hierarchy(subclass, indent + 2)

# print_hierarchy(BaseException)

Custom Exceptions

Basic Custom Exceptions

# Base custom exception
class AppError(Exception):
    """Base exception for application."""
    
    def __init__(self, message: str, code: int = None):
        super().__init__(message)
        self.message = message
        self.code = code
    
    def to_dict(self):
        return {
            "error": self.__class__.__name__,
            "message": self.message,
            "code": self.code
        }

# Specific exceptions
class ValidationError(AppError):
    """Validation error."""
    
    def __init__(self, field: str, message: str):
        super().__init__(f"Validation failed for {field}: {message}")
        self.field = field
        self.code = 400

class NotFoundError(AppError):
    """Resource not found."""
    
    def __init__(self, resource: str, identifier):
        super().__init__(f"{resource} with id {identifier} not found")
        self.resource = resource
        self.identifier = identifier
        self.code = 404

class DatabaseError(AppError):
    """Database operation failed."""
    
    def __init__(self, operation: str, reason: str):
        super().__init__(f"Database {operation} failed: {reason}")
        self.code = 500

class AuthenticationError(AppError):
    """Authentication failed."""
    
    def __init__(self, message: str = "Authentication required"):
        super().__init__(message)
        self.code = 401

class AuthorizationError(AppError):
    """Authorization failed."""
    
    def __init__(self, permission: str):
        super().__init__(f"Missing permission: {permission}")
        self.permission = permission
        self.code = 403

# Usage
def validate_user(user_data: dict):
    """Validate user data."""
    if "name" not in user_data:
        raise ValidationError("name", "required")
    
    if "email" not in user_data:
        raise ValidationError("email", "required")
    
    if len(user_data["name"]) < 2:
        raise ValidationError("name", "too short")
    
    return True

# Handle exceptions
try:
    validate_user({"name": "A"})
except ValidationError as e:
    print(f"Error: {e}")
    print(f"Field: {e.field}")
    print(f"Code: {e.code}")

Output:

Architecture Diagram
Error: Validation failed for name: too short
Field: name
Code: 400

Exception Chaining

class ServiceError(AppError):
    """Service layer error."""
    pass

class UserService:
    def get_user(self, user_id: int) -> dict:
        try:
            # Database call
            user = {"id": user_id, "name": "Alice"}
            return user
        except DatabaseError as e:
            # Chain exceptions
            raise ServiceError(f"Failed to get user: {e}") from e

# Usage
service = UserService()
try:
    user = service.get_user(1)
except ServiceError as e:
    print(f"Service error: {e}")
    print(f"Original cause: {e.__cause__}")

Exception Groups (Python 3.11+)

# Exception groups for multiple errors
def validate_multiple(data: dict) -> list:
    """Validate multiple fields, collect all errors."""
    errors = []
    
    if "name" not in data:
        errors.append(ValidationError("name", "required"))
    
    if "email" not in data:
        errors.append(ValidationError("email", "required"))
    
    if "age" in data and data["age"] < 0:
        errors.append(ValidationError("age", "must be positive"))
    
    if errors:
        raise ExceptionGroup("Validation failed", errors)
    
    return []

# Usage (Python 3.11+)
try:
    validate_multiple({})
except* ValidationError as eg:
    for error in eg.exceptions:
        print(f"Validation error: {error}")

πŸ’‘

Interview Tip: Use exception chaining (raise X from Y) to preserve the original error context.


Context Managers for Error Handling

Resource Management

from contextlib import contextmanager
from typing import Generator, Any

@contextmanager
def error_handler():
    """Context manager for error handling."""
    try:
        yield
    except ValueError as e:
        print(f"ValueError caught: {e}")
    except TypeError as e:
        print(f"TypeError caught: {e}")
    except Exception as e:
        print(f"Unexpected error: {e}")
        raise  # Re-raise unexpected errors

# Usage
with error_handler():
    value = int("not a number")
print("Continues after error handling")

# Database transaction context manager
@contextmanager
def transaction(session):
    """Transaction context manager with rollback."""
    try:
        yield session
        session.commit()
    except Exception as e:
        session.rollback()
        raise AppError(f"Transaction failed: {e}") from e
    finally:
        session.close()

# Usage
# with transaction(session) as sess:
#     sess.add(user)
#     sess.commit()  # Automatic on success

Suppressed Exceptions

from contextlib import suppress
import os

# Suppress specific exceptions
with suppress(FileNotFoundError):
    os.remove("nonexistent.txt")
print("Continues even if file doesn't exist")

# Multiple suppressions
with suppress(FileNotFoundError, PermissionError):
    os.remove("protected.txt")

# Equivalent to:
try:
    os.remove("nonexistent.txt")
except FileNotFoundError:
    pass

Exception Best Practices

Specific Exceptions

# BAD: Catching all exceptions
def bad_example():
    try:
        do_something()
    except:
        pass  # Swallows all exceptions!

# BAD: Catching broad exceptions
def bad_example2():
    try:
        do_something()
    except Exception as e:
        print(f"Error: {e}")

# GOOD: Catch specific exceptions
def good_example():
    try:
        do_something()
    except ValueError as e:
        print(f"Value error: {e}")
    except TypeError as e:
        print(f"Type error: {e}")
    except Exception as e:
        print(f"Unexpected error: {e}")
        raise  # Re-raise unexpected errors

# GOOD: Handle known exceptions, let others propagate
def good_example2():
    try:
        result = int(user_input)
    except ValueError:
        return None  # Handle expected error
    # ValueError is handled, others propagate

Exception Information

import sys
import traceback

def get_exception_info():
    """Get detailed exception information."""
    try:
        1 / 0
    except Exception as e:
        # Get exception type and message
        print(f"Exception type: {type(e).__name__}")
        print(f"Exception message: {e}")
        
        # Get traceback
        tb = sys.exc_info()[2]
        print(f"Line number: {tb.tb_lineno}")
        print(f"File: {tb.tb_frame.f_code.co_filename}")
        
        # Get full traceback as string
        tb_str = traceback.format_exc()
        print(f"Full traceback:\n{tb_str}")
        
        # Get exception with context
        exc_info = sys.exc_info()
        print(f"Exception info: {exc_info}")

get_exception_info()

Logging Exceptions

import logging
import traceback

logging.basicConfig(level=logging.INFO)
logger = logging.getLogger(__name__)

def process_with_logging():
    """Process with proper exception logging."""
    try:
        result = risky_operation()
        return result
    except ValidationError as e:
        # Log at warning level for expected errors
        logger.warning(f"Validation failed: {e}")
        return None
    except Exception as e:
        # Log at error level for unexpected errors
        logger.error(f"Unexpected error: {e}")
        logger.error(f"Traceback: {traceback.format_exc()}")
        raise

# Custom exception handler
class ExceptionLogger:
    """Log exceptions with context."""
    
    def __init__(self, operation: str):
        self.operation = operation
    
    def __enter__(self):
        return self
    
    def __exit__(self, exc_type, exc_val, exc_tb):
        if exc_type is not None:
            logger.error(
                f"Operation '{self.operation}' failed",
                exc_info=(exc_type, exc_val, exc_tb)
            )
        return False

# Usage
with ExceptionLogger("user_creation"):
    create_user(user_data)

⚠️

Common Mistake: Swallowing exceptions with bare except: or except Exception: pass hides bugs.


Advanced Patterns

Retry Pattern

import time
from typing import Type, Tuple
from functools import wraps

def retry(
    max_attempts: int = 3,
    delay: float = 1.0,
    backoff: float = 2.0,
    exceptions: Tuple[Type[Exception], ...] = (Exception,)
):
    """Retry decorator with exponential backoff."""
    def decorator(func):
        @wraps(func)
        def wrapper(*args, **kwargs):
            last_exception = None
            
            for attempt in range(max_attempts):
                try:
                    return func(*args, **kwargs)
                except exceptions as e:
                    last_exception = e
                    
                    if attempt < max_attempts - 1:
                        wait_time = delay * (backoff ** attempt)
                        print(f"Attempt {attempt + 1} failed: {e}")
                        print(f"Retrying in {wait_time:.1f}s...")
                        time.sleep(wait_time)
            
            raise last_exception
        return wrapper
    return decorator

# Usage
@retry(max_attempts=3, delay=0.1, exceptions=(ValueError, ConnectionError))
def unreliable_operation():
    import random
    if random.random() < 0.7:
        raise ValueError("Random failure")
    return "Success!"

try:
    result = unreliable_operation()
    print(f"Result: {result}")
except ValueError as e:
    print(f"Final failure: {e}")

Circuit Breaker Pattern

import time
from enum import Enum
from typing import Callable

class CircuitState(Enum):
    CLOSED = "closed"
    OPEN = "open"
    HALF_OPEN = "half_open"

class CircuitBreaker:
    """Circuit breaker for fault tolerance."""
    
    def __init__(
        self,
        failure_threshold: int = 5,
        recovery_timeout: float = 30.0,
        success_threshold: int = 3
    ):
        self.failure_threshold = failure_threshold
        self.recovery_timeout = recovery_timeout
        self.success_threshold = success_threshold
        
        self.failure_count = 0
        self.success_count = 0
        self.last_failure_time = None
        self.state = CircuitState.CLOSED
    
    def record_success(self):
        """Record successful operation."""
        self.success_count += 1
        if self.state == CircuitState.HALF_OPEN:
            if self.success_count >= self.success_threshold:
                self.state = CircuitState.CLOSED
                self.failure_count = 0
                self.success_count = 0
    
    def record_failure(self):
        """Record failed operation."""
        self.failure_count += 1
        self.last_failure_time = time.time()
        
        if self.failure_count >= self.failure_threshold:
            self.state = CircuitState.OPEN
    
    def can_execute(self) -> bool:
        """Check if operation can be executed."""
        if self.state == CircuitState.CLOSED:
            return True
        elif self.state == CircuitState.OPEN:
            if time.time() - self.last_failure_time >= self.recovery_timeout:
                self.state = CircuitState.HALF_OPEN
                return True
            return False
        else:  # HALF_OPEN
            return True

def call_with_circuit_breaker(
    func: Callable,
    circuit_breaker: CircuitBreaker,
    *args,
    **kwargs
):
    """Execute function with circuit breaker."""
    if not circuit_breaker.can_execute():
        raise AppError("Circuit breaker is OPEN")
    
    try:
        result = func(*args, **kwargs)
        circuit_breaker.record_success()
        return result
    except Exception as e:
        circuit_breaker.record_failure()
        raise

# Usage
circuit = CircuitBreaker(failure_threshold=3, recovery_timeout=5.0)

def unreliable_api():
    import random
    if random.random() < 0.5:
        raise ConnectionError("API failed")
    return "OK"

# Execute with circuit breaker
try:
    for i in range(10):
        result = call_with_circuit_breaker(unreliable_api, circuit)
        print(f"Request {i+1}: {result}")
except AppError as e:
    print(f"Circuit breaker: {e}")

Exception Factory

from typing import Dict, Type, Any

class ExceptionFactory:
    """Factory for creating exceptions."""
    
    _exceptions: Dict[str, Type[AppError]] = {
        "validation": ValidationError,
        "not_found": NotFoundError,
        "database": DatabaseError,
        "auth": AuthenticationError,
        "permission": AuthorizationError,
    }
    
    @classmethod
    def create(cls, exc_type: str, **kwargs) -> AppError:
        """Create exception by type."""
        exception_class = cls._exceptions.get(exc_type)
        if not exception_class:
            raise ValueError(f"Unknown exception type: {exc_type}")
        
        return exception_class(**kwargs)
    
    @classmethod
    def register(cls, name: str, exception_class: Type[AppError]):
        """Register new exception type."""
        cls._exceptions[name] = exception_class

# Usage
error = ExceptionFactory.create("validation", field="email", message="required")
print(f"Created: {error}")

error = ExceptionFactory.create("not_found", resource="User", identifier=123)
print(f"Created: {error}")

Interview Tips

Common Follow-up Questions

  1. "When should you catch exceptions vs let them propagate?"

    • Catch: When you can handle the error
    • Propagate: When caller should handle it
    • Log: Always log before re-raising
  2. "What's the difference between except Exception as e and except:?"

    • as e: Captures exception object
    • Bare except: Catches everything (including SystemExit)
    • Always use as e and catch specific exceptions
  3. "How do you handle multiple exceptions?"

    • Separate except blocks for different types
    • Tuple of exceptions: except (ValueError, TypeError)
    • Exception groups (Python 3.11+)

Code Review Tips

# BAD: Bare except
try:
    do_something()
except:
    pass

# GOOD: Specific exception
try:
    do_something()
except ValueError as e:
    handle_value_error(e)

# BAD: Swallowing exceptions
try:
    do_something()
except Exception:
    return None  # Hides the error!

# GOOD: Log and re-raise
try:
    do_something()
except Exception as e:
    logger.error(f"Error: {e}")
    raise

# BAD: Exception in except block
try:
    do_something()
except Exception as e:
    do_something_else()  # What if this fails?

# GOOD: Handle all cases
try:
    do_something()
except Exception as e:
    try:
        do_something_else()
    except Exception:
        logger.error("Failed to handle error")

ℹ️

Best Practice: Always log exceptions before re-raising. Include context for debugging.


Summary

PatternPurposeWhen to Use
EAFPPythonic error handlingPrefer over LBYL
Custom ExceptionsDomain-specific errorsBusiness logic
Context ManagersResource cleanupAlways
RetryFault toleranceTransient failures
Circuit BreakerPrevent cascade failuresExternal services

Best Practices

  1. Use EAFP over LBYL
  2. Catch specific exceptions
  3. Log exceptions with context
  4. Use context managers for cleanup
  5. Create custom exceptions for domain errors
  6. Implement retry for transient failures
  7. Use circuit breaker for external services

ℹ️

Key Takeaway: Proper exception handling makes code robust, maintainable, and debuggable.


Practice Problems

  1. Exception Hierarchy: Design a custom exception hierarchy for an e-commerce system
  2. Retry Decorator: Implement retry with exponential backoff
  3. Circuit Breaker: Build a circuit breaker pattern
  4. Error Handler: Create an error handler with logging and notification
  5. Validation System: Implement a validation system with custom exceptions

Further Reading

  • Python Docs: Exceptions, context managers
  • PEP 3151: Reworking the OS and IO exception hierarchy
  • Books: "Python Cookbook" by David Beazley
  • Patterns: Error handling patterns in distributed systems

Remember: Good exception handling is about being explicit, informative, and resilient.

Advertisement