Python Exceptions & Error Handling

🐍 Python 3.12+ 🟒 Chapter 25 of 65 πŸ“‚ Phase 6: Exception and File Handling πŸ“… 2026 Edition
πŸ“Œ Covered in this chapter: Errors vs Exceptions Β· try-except-else-finally Β· Multiple Exceptions Β· raise Β· Custom Exceptions Β· Hierarchy Β· Useful Messages
Master robust defensive programming in Python: understanding syntax errors vs runtime exceptions, the complete 4-clause try-except-else-finally lifecycle, exception hierarchy, custom exception classes, and writing actionable error messages.
1Syntax Errors vs Runtime Exceptions & The Exception Hierarchy

In Python, defects in code fall into two distinct categories:

  1. Syntax Errors (Compile-Time): Occur during the parsing stage before the program begins executing. If your code contains a missing colon (if x == 5) or mismatched parenthesis, Python immediately halts with a SyntaxError or IndentationError. Syntax errors cannot be caught with try-except.
  2. Exceptions (Runtime): Occur while a syntactically valid program is actively executing. For example, dividing by zero (ZeroDivisionError), accessing a missing dictionary key (KeyError), or opening a non-existent file (FileNotFoundError).

The Python Exception Class Hierarchy: All built-in exceptions in Python form an inheritance tree rooted at BaseException:

β”Œβ”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β” β”‚ BaseException β”‚ β”‚ β”œβ”€β”€ SystemExit, KeyboardInterrupt (Ctrl+C), GeneratorExitβ”‚ β”‚ └── Exception (The root for all application errors) β”‚ β”‚ β”œβ”€β”€ ArithmeticError (ZeroDivisionError, Overflow) β”‚ β”‚ β”œβ”€β”€ LookupError (IndexError, KeyError) β”‚ β”‚ β”œβ”€β”€ TypeError, ValueError, NameError β”‚ β”‚ └── OSError (FileNotFoundError, PermissionError) β”‚ β””β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”˜
⚠️ Rule: Never Catch BaseException Directly:
Always inherit your custom exceptions from Exception (not BaseException). Catching BaseException will unintentionally block user keyboard interrupts (Ctrl+C / KeyboardInterrupt) and graceful system shutdowns.
πŸ’» Example 1: Inspecting Python Built-in Exception Hierarchy
# Inspecting the Exception Class Hierarchy at runtime:
print("Is ZeroDivisionError an ArithmeticError?", issubclass(ZeroDivisionError, ArithmeticError))
print("Is KeyError a LookupError?", issubclass(KeyError, LookupError))
print("Is FileNotFoundError an OSError?", issubclass(FileNotFoundError, OSError))
print("Is ArithmeticError an Exception?", issubclass(ArithmeticError, Exception))
πŸ” Inheritance Polymorphism:

Because KeyError inherits from LookupError, catching except LookupError: will catch both KeyError and IndexError with a single handler!

2The 4-Clause Lifecycle: try, except, else, finally

A complete, professional error handling block in Python can utilize up to four distinct clauses:

  • try: Encloses the risky code that might raise an exception.
  • except ExceptionType as err: Executes ONLY if a matching exception occurs inside the try block.
  • else: Executes ONLY if the try block completed successfully with ZERO exceptions. (Keeps try blocks minimal!).
  • finally: Executes unconditionally 100% of the time, regardless of whether an exception occurred, was caught, or if a return statement was encountered. Used for critical cleanup (closing database connections, releasing file locks).
β”Œβ”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β” β”‚ try block β”‚ β””β”€β”€β”€β”€β”€β”€β”€β”¬β”€β”€β”€β”€β”€β”€β”€β”˜ β”‚ Exception? ──▢ YES ──▢ β”Œβ”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β” β”‚ β”‚ except block β”‚ NO β””β”€β”€β”€β”€β”€β”€β”€β”€β”¬β”€β”€β”€β”€β”€β”€β”€β”˜ β”‚ β”‚ β–Ό β”‚ β”Œβ”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β” β”‚ β”‚ else block β”‚ β”‚ β””β”€β”€β”€β”€β”€β”€β”€β”¬β”€β”€β”€β”€β”€β”€β”€β”˜ β”‚ β”‚ β”‚ β–Ό β–Ό β”Œβ”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β” β”‚ finally block β”‚ (Guaranteed 100% Execution) β””β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”˜
πŸ’» Example 2: The Complete 4-Clause try-except-else-finally Flow
def safe_divide_engine(a, b):
    print(f"\n--- Attempting: {a} / {b} ---")
    try:
        # Step 1: Risky calculation
        result = a / b
    except ZeroDivisionError as err:
        # Step 2: Handles zero division error
        print(f"❌ Handled Error: Cannot divide by zero! ({err})")
        return None
    except TypeError as err:
        # Step 2: Handles invalid data types
        print(f"❌ Handled Error: Both operands must be numbers! ({err})")
        return None
    else:
        # Step 3: Runs ONLY if division was successful
        print(f"βœ… Calculation Successful! Result = {result}")
        return result
    finally:
        # Step 4: Runs ALWAYS for resource cleanup / logging
        print("πŸ”’ [Cleanup] safe_divide_engine execution cycle finished.")

# Test all branches:
safe_divide_engine(100, 4)   # Success (try -> else -> finally)
safe_divide_engine(50, 0)    # Zero Division (try -> except -> finally)
safe_divide_engine(10, "2")  # Type Error (try -> except -> finally)
πŸ” Step-by-Step Flow:
  • In safe_divide_engine(100, 4): try succeeds, else prints the result, and finally executes cleanup.
  • In safe_divide_engine(50, 0): ZeroDivisionError triggers, except handles it, else is skipped, and finally executes before returning None.
3Handling Multiple Specific Exceptions & Grouping

Never use a bare except: or generic except Exception: unless logging at the top-level boundary. Catching specific exceptions prevents masking unrelated programming bugs.

You can define multiple dedicated except blocks, or group related exceptions into a single tuple except (ValueError, TypeError) as err::

πŸ’» Example 3: Handling Multiple Specific Exceptions and Tuple Grouping
def process_user_record(data_dict, key, divisor):
    try:
        raw_val = data_dict[key]          # May raise KeyError
        parsed_num = float(raw_val)       # May raise ValueError
        computed = 1000 / parsed_num      # May raise ZeroDivisionError
        return computed
    except KeyError:
        print(f"❌ Error: Required key '{key}' is missing from user record!")
    except (ValueError, TypeError) as err:
        print(f"❌ Data Parsing Error: Invalid numeric input ({err})")
    except ZeroDivisionError:
        print("❌ Math Error: Value cannot be zero!")

# Testing different failure scenarios:
user_data = {"score": "50", "bonus": "zero", "zero_val": "0"}

print("1. Valid score:    ", process_user_record(user_data, "score", 2))
print("2. Missing key:    ", process_user_record(user_data, "missing_key", 2))
print("3. Bad number string:", process_user_record(user_data, "bonus", 2))
print("4. Division by zero:", process_user_record(user_data, "zero_val", 2))
πŸ” Why Specific Handlers Matter:

Specific exception blocks allow you to provide targeted recovery actions (e.g. asking for missing keys vs asking for re-entered numbers) rather than failing generically.

4Raising Exceptions & Exception Chaining (raise ... from)

Use the raise statement to throw an exception when business rule invariants are violated. Python 3 also supports Exception Chaining via raise NewException from original_error, preserving the original root cause traceback for debugging:

πŸ’» Example 4: Explicit Exception Raising and Guarding Business Logic
def validate_account_age(age):
    if not isinstance(age, int):
        raise TypeError(f"Age must be an integer, received {type(age).__name__}")
    if age < 0:
        raise ValueError(f"Age cannot be negative! Received: {age}")
    if age < 18:
        raise PermissionError(f"User is {age} years old. Minimum required age is 18.")
    return "βœ… Age verified successfully!"

# Test exception raising:
try:
    print(validate_account_age(22))  # Valid
    validate_account_age(-5)         # Triggers ValueError
except (TypeError, ValueError, PermissionError) as err:
    print(f"🚫 Validation Failed: {err}")
πŸ” Fail-Fast Principle:

Raising exceptions immediately when invalid inputs arrive prevents corrupted state from propagating deeper into your application and database.

5Creating Custom Application Exceptions (OOP Hierarchy)

In large production applications (FastAPI backends, payment gateways, microservices), you should define custom domain-specific exception classes by inheriting from Python's standard Exception class:

πŸ’» Example 5: Designing Custom Application Exceptions
# Define Custom Application Exceptions
class BankingAppError(Exception):
    """Base exception for all banking domain errors."""
    pass

class InsufficientFundsError(BankingAppError):
    """Raised when a withdrawal exceeds available balance."""
    def __init__(self, balance, amount_requested):
        self.balance = balance
        self.amount_requested = amount_requested
        self.shortage = amount_requested - balance
        super().__init__(
            f"Withdrawal of Rs.{amount_requested:.2f} failed! "
            f"Current Balance: Rs.{balance:.2f} (Short by Rs.{self.shortage:.2f})"
        )

class AccountLockedError(BankingAppError):
    """Raised when operating on a frozen account."""
    pass

def withdraw_money(account_balance, amount, is_locked=False):
    if is_locked:
        raise AccountLockedError("Account is temporarily locked. Please contact support.")
    if amount > account_balance:
        raise InsufficientFundsError(account_balance, amount)
    return account_balance - amount

# Test Custom Exception handling:
try:
    new_bal = withdraw_money(500.0, 750.0)
except InsufficientFundsError as err:
    print("πŸ’³ Transaction Blocked:", err)
    print(f"πŸ‘‰ Customer needs to deposit at least Rs.{err.shortage:.2f} more.")
except BankingAppError as err:
    print("🏦 General Banking Error:", err)
πŸ” Architectural Advantage:

Custom exceptions carry rich structured metadata (like err.shortage) allowing API layers to return structured HTTP 400/403 JSON responses to frontend clients automatically.

⚠️ Common Developer Pitfall: Using Bare "except:" or "except Exception: pass" (The Silent Bug Trap)

Using a bare "except:" or catching Exception and doing "pass" silently suppresses all errors including typos (NameError), keyboard interrupts, and out-of-memory errors, making code completely impossible to debug. Always catch specific exceptions and log them!

πŸ’» Hands-on Interactive Practice Challenge

Write a function parse_integer_input(prompt_text) that repeatedly prompts the user in a while loop until they provide a valid integer, handling ValueError gracefully.

Python 3 Practice Challenge β–Ά Run in Compiler
def safe_int_converter(raw_value):
    try:
        return int(raw_value), "Success"
    except ValueError:
        return None, f"'{raw_value}' is not a valid integer!"

# Test inputs:
for test_val in ["42", "hello", "100.5", "-99"]:
    val, status = safe_int_converter(test_val)
    print(f"Input: {test_val:8} -> Parsed: {val} ({status})")
Run This Challenge in Online Python IDE β†’
❓ Frequently Asked Questions (FAQ)

Q What is the difference between else and finally in try-except?

The else block runs ONLY if the try block succeeds with zero exceptions. The finally block runs unconditionally 100% of the time, even if an unhandled exception occurred or a return statement was executed.

Q Why should custom exceptions inherit from Exception and not BaseException?

BaseException is the root of system-level exits like KeyboardInterrupt and SystemExit. Inheriting from Exception ensures your errors represent application issues without interfering with process termination.

Q How do I re-raise the currently active exception?

Inside an except block, simply write "raise" with no arguments. Python will re-raise the active exception up the call stack with its full original traceback intact.

OC
Written by Our Compiler Technical Editorial Team
Reviewed for accuracy & tested on Python 3.12+ runtime Β· Last updated August 2026