🛡️ DevAscent Academy • Core Python Textbook Series

Unit 11: Exception Handling

The definitive guide to writing bulletproof, crash-resistant software in Python. Understand how programs fail, distinguish between Syntax, Runtime, and Logical errors, master defensive programming using try, except, else, and finally, intercept multiple targeted exceptions, enforce business validation rules with raise, and architect custom user-defined exception hierarchies.

Curriculum Level 1st Year BCA / B.Tech / Diploma
Unit Code PY-111 (Defensive Programming)
Content Balance 50% Theory • 30% Code • 20% Flowcharts
Target Standard University Exam & Enterprise Ready
📐 Formula Verified: 50% Comprehensive Theory 30% Verified Code 20% Visual Flowcharts
1

Types of Errors: Syntax Errors, Runtime Errors, and Logical Errors

📌 What is an Error in Computer Programming?

An error (often called a bug) is a flaw or unexpected condition in a computer program that prevents it from executing successfully or causes it to generate incorrect, abnormal, or unintended results. In Python, errors are classified into three distinct categories based on when they occur and how the Python interpreter responds.

📐 FLOWCHART 1: THE TAXONOMY OF ERRORS IN PYTHON Root Classification
1. Syntax Errors
Detected at Parse Time before execution begins. Code violates grammatical Python rules (missing colon, typos). Program will NOT start!
➔
2. Runtime Errors (Exceptions)
Detected at Execution Time. Syntax is 100% valid, but an illegal operation occurs (div by 0, missing file). Can be caught with try-except!
➔
3. Logical Errors
Silent Semantic Flaws. Program runs without any error messages, but output is mathematically wrong! Caused by human algorithm mistakes.

1.1 Syntax Errors (Compile-Time / Parsing Errors)

A Syntax Error occurs when the programmer writes code that violates the grammar and structural rules of Python. The CPython parser detects these errors during the parsing phase before a single line of bytecode is executed.

1.2 Runtime Errors (Exceptions)

A Runtime Error (formally known as an Exception) occurs while the program is actively running. The syntax is completely valid, but Python encounters a situation it cannot legally process.

1.3 Logical Errors (Semantic Bugs)

A Logical Error is the most dangerous kind of error because Python will not throw any error message, warning, or crash. The code runs to completion, but the output is incorrect due to a flaw in the programmer's algorithm or business formula.

Feature Syntax Error Runtime Error (Exception) Logical Error
When Detected? Before execution (Parsing phase). During execution (Runtime). Never detected by interpreter (Found by human testing).
Does Program Run? No, refuses to start. Runs until the faulty line is hit, then crashes. Runs fully to completion without crashing.
Catchable with try-except? ❌ No ✅ Yes (100% Catchable) ❌ No (Produces wrong output silently)
Typical Example if x == 5 (missing colon) 10 / 0 (ZeroDivisionError) area = length + width (wrong formula)
2

Defensive Programming: The `try` and `except` Block

📌 What is Exception Handling?

Exception Handling is a programming mechanism that allows a software application to intercept runtime errors, handle them gracefully, and continue executing without abruptly crashing or terminating. Instead of crashing and showing ugly traceback errors to end users, the program displays friendly messages or takes corrective actions.

🚗 The Automobile Airbag Analogy: Driving a car without exception handling is like driving without seatbelts or airbags: a minor collision immediately smashes the windshield and halts your journey.
The try...except block acts like a modern vehicle's airbag system: when a sudden impact (an exception) occurs, the airbag deploys instantly, cushions the blow, protects the passengers, and allows you to safely steer to the shoulder!
📐 FLOWCHART 2: THE try...except CONTROL FLOW ARCHITECTURE Execution Interception
1. Enter try Block
Execute risky statements (user input, math division, file opening).
➔ Did Error Occur? ➔
NO Error Occurred
Skip all except blocks completely. Program continues smooth normal execution!
▼ YES: Exception Raised! Immediate Jump ▼
2. Intercepted by matching except Block!
CPython halts the try block immediately and transfers control to the matching except handler. Crash is prevented!
🐍 01_basic_try_except.py
# Case A: Crashing without Exception Handling (Anti-Pattern)
# num = 10 / 0  # CRASH! Program terminates immediately with ZeroDivisionError

# Case B: Graceful recovery using try-except
print("System starting...")

try:
    numerator = 50
    denominator = 0  # Will cause division error
    result = numerator / denominator
    print(f"Result: {result}")  # This line is skipped!
except ZeroDivisionError:
    print("⚠️ HANDLED: Cannot divide a number by zero! Defaulting to 0.")
    result = 0

print(f"Final safe value: {result}")
print("System continues running smoothly without crashing! 🎉")
Terminal Output
System starting... ⚠️ HANDLED: Cannot divide a number by zero! Defaulting to 0. Final safe value: 0 System continues running smoothly without crashing! 🎉
3

Handling Multiple Exceptions: Specificity and Grouped Handlers

📌 Why Specificity Matters (The Golden Rule of Exception Handling)

A single block of code can fail in multiple different ways. For example, asking a user for a number and dividing 100 by it can fail with a ValueError (if they type "abc") OR a ZeroDivisionError (if they type 0).
Best Practice Rule: Always catch specific exceptions individually rather than using a broad, blind except:. Catching specific errors allows you to give accurate diagnostic feedback for each unique failure.

🐍 02_multiple_exceptions.py
def process_student_marks(scores_list, index_str):
    try:
        idx = int(index_str)                    # May raise ValueError
        val = scores_list[idx]                  # May raise IndexError
        percentage = 100 / val                  # May raise ZeroDivisionError
        print(f"Success: Index [{idx}] -> Score: {val} | Factor: {percentage:.2f}")

    except ValueError:
        print(f"❌ Input Error: '{index_str}' is not a valid integer!")

    except IndexError:
        print(f"❌ Range Error: Index {index_str} is out of bounds (List size: {len(scores_list)})!")

    except ZeroDivisionError:
        print("❌ Arithmetic Error: Score at this position is 0, cannot divide by zero!")

    except Exception as err:
        print(f"❌ Unexpected Error: {type(err).__name__} -> {err}")

marks = [85, 0, 92]

# Test Scenario 1: Valid Execution
process_student_marks(marks, "0")

# Test Scenario 2: ValueError
process_student_marks(marks, "two")

# Test Scenario 3: IndexError
process_student_marks(marks, "9")

# Test Scenario 4: ZeroDivisionError
process_student_marks(marks, "1")
Terminal Output
Success: Index [0] -> Score: 85 | Factor: 1.18 ❌ Input Error: 'two' is not a valid integer! ❌ Range Error: Index 9 is out of bounds (List size: 3)! ❌ Arithmetic Error: Score at this position is 0, cannot divide by zero!
4

The Complete 4-Part Structure: `try`, `except`, `else`, and `finally`

📐 FLOWCHART 3: THE COMPLETE 4-STAGE EXCEPTION PIPELINE Full try-except-else-finally Flow
1. try Block
Run risky code.
➔ Failed? ➔
2. except Block
Runs ONLY if an error occurred.
▼ No Error in try? Run else! ▼
3. else Block
Runs ONLY if NO exception was raised in try!
➔ Then Always ➔
4. finally Block
GUARANTEED TO RUN under all conditions (even after crashes or returns!).
Clause When Does It Execute? Primary Engineering Purpose
try Always executed first. Isolates code that carries a risk of runtime failure.
except Executed ONLY IF an exception occurs in try. Catches and handles the error gracefully.
else Executed ONLY IF NO exception occurs in try. Executes follow-up logic that requires successful try completion.
finally ALWAYS EXECUTED, without exception. Resource cleanup: closing files, network sockets, database connections.
🐍 03_try_except_else_finally.py
def safe_file_reader(filename):
    print(f"\n--- Attempting to open '{filename}' ---")
    file = None
    try:
        file = open(filename, "r", encoding="utf-8")
        content = file.read()
    except FileNotFoundError:
        print(f"EXCEPT: File '{filename}' does not exist on disk!")
    else:
        print(f"ELSE: File successfully read! Size: {len(content)} characters.")
    finally:
        if file and not file.closed:
            file.close()
            print("FINALLY: File handle safely closed in RAM.")
        else:
            print("FINALLY: Cleanup routine completed.")

# Case 1: Missing file (triggers try -> except -> finally)
safe_file_reader("non_existent_doc.txt")

# Case 2: Existing file (triggers try -> else -> finally)
with open("test_doc.txt", "w") as f:
    f.write("DevAscent Academy")

safe_file_reader("test_doc.txt")
Terminal Output
--- Attempting to open 'non_existent_doc.txt' --- EXCEPT: File 'non_existent_doc.txt' does not exist on disk! FINALLY: Cleanup routine completed. --- Attempting to open 'test_doc.txt' --- ELSE: File successfully read! Size: 17 characters. FINALLY: File handle safely closed in RAM.
5

Raising Exceptions: The `raise` Keyword & Business Rule Enforcement

📌 Why Manually Raise Exceptions?

Python automatically triggers exceptions for system-level errors like dividing by zero or missing files. But what if a user enters an age of -25 or an ATM withdrawal of -₹5,000? To Python, negative numbers are completely valid integers! However, to your business logic, a negative age or withdrawal is illegal.
The raise keyword allows developers to intentionally trigger an exception when business validation rules are broken.

📐 FLOWCHART 4: THE `raise` BUSINESS VALIDATION PIPELINE Intentional Exception Trigger
1. Incoming Data Input
User submits application: age = -15
➔ Test Rule ➔
2. Check: age < 0 ?
Condition is True! raise ValueError("Age cannot be negative!") triggers immediately.
➔ Catch ➔
3. Caller Interception
Caller's except ValueError: handles rejection cleanly without corrupting the database!
🐍 04_raising_exceptions.py
def verify_driving_license_eligibility(applicant_name, age):
    """Enforces biological and legal constraints using raise."""
    if not isinstance(age, int):
        raise TypeError(f"Age must be an integer number, received: {type(age).__name__}")
    
    if age < 0 or age > 120:
        raise ValueError(f"Invalid biological age: {age}. Must be between 0 and 120.")
    
    if age < 18:
        raise PermissionError(f"Underage applicant: {applicant_name} ({age} yrs). Minimum age is 18.")
    
    return f"✅ APPROVED: {applicant_name} is eligible to take the driving test!"

# Testing defensive validation
test_cases = [
    ("Suresh Kumar", 22),
    ("Aman Gupta", 16),
    ("Pooja Roy", -5)
]

for name, age_val in test_cases:
    try:
        verdict = verify_driving_license_eligibility(name, age_val)
        print(verdict)
    except (ValueError, PermissionError, TypeError) as err:
        print(f"🛑 REJECTED: {err}")
Terminal Output
✅ APPROVED: Suresh Kumar is eligible to take the driving test! 🛑 REJECTED: Underage applicant: Aman Gupta (16 yrs). Minimum age is 18. 🛑 REJECTED: Invalid biological age: -5. Must be between 0 and 120.
6

Creating Custom (User-Defined) Exceptions

📌 What is a Custom Exception?

While Python's standard library includes dozens of built-in exceptions, complex enterprise applications often require domain-specific error types like InsufficientBalanceError, InvalidTokenError, or SeatUnavailableError.
In Python, a custom exception is simply a class that inherits from the built-in Exception class (or one of its subclasses).

📐 FLOWCHART 5: CUSTOM EXCEPTION INHERITANCE HIERARCHY Object-Oriented Subclassing
BaseException (Root)
CPython root exception class (handles SystemExit, KeyboardInterrupt).
➔ Subclasses ➔
Exception (Standard Root)
Base class for all non-fatal application exceptions.
➔ You Inherit ➔
Your Custom Exception
class InsufficientFundsError(Exception): pass
🐍 05_custom_exceptions.py
# Step 1: Define custom exception classes inheriting from Exception
class BankingException(Exception):
    """Base exception for all banking errors."""
    pass

class InsufficientFundsError(BankingException):
    """Raised when withdrawal exceeds available account balance."""
    def __init__(self, balance, requested):
        self.balance = balance
        self.requested = requested
        super().__init__(f"Deficit: Balance is ₹{balance}, cannot withdraw ₹{requested}!")

class DailyLimitExceededError(BankingException):
    """Raised when withdrawal exceeds ₹50,000 daily ATM cap."""
    pass

# Step 2: Use custom exceptions inside business logic
def withdraw_atm_cash(account_balance, amount):
    if amount > 50000:
        raise DailyLimitExceededError(f"₹{amount} exceeds daily ATM withdrawal limit of ₹50,000!")
    
    if amount > account_balance:
        raise InsufficientFundsError(account_balance, amount)
    
    new_balance = account_balance - amount
    return new_balance

# Step 3: Handling custom exceptions
account_balance = 15000

# Test Scenario 1: Attempting to withdraw ₹25,000 (Insufficient Funds)
try:
    account_balance = withdraw_atm_cash(account_balance, 25000)
except InsufficientFundsError as ife:
    print("ATM Alert:", ife)

# Test Scenario 2: Attempting to withdraw ₹80,000 (Exceeds daily cap)
try:
    account_balance = withdraw_atm_cash(account_balance, 80000)
except DailyLimitExceededError as dfe:
    print("ATM Alert:", dfe)
Terminal Output
ATM Alert: Deficit: Balance is ₹15000, cannot withdraw ₹25000! ATM Alert: ₹80000 exceeds daily ATM withdrawal limit of ₹50,000!
7

Unit 11 Practical Capstone: E-Commerce Checkout Engine

This capstone combines all Unit 11 concepts: custom exception classes, validation checks with raise, multiple targeted except handlers, positive progression in else, and cleanup/receipt finalization in finally.

🐍 ecommerce_checkout_engine.py
class OutOfStockError(Exception): pass
class PaymentDeclinedError(Exception): pass

INVENTORY = {"Laptop": 3, "Mouse": 0, "Headphones": 5}

def process_customer_order(item_name, quantity, wallet_balance, item_price):
    print(f"\n==============================================")
    print(f"🛒 PROCESSING ORDER: {quantity}x {item_name}")
    print(f"==============================================")
    
    order_success = False
    total_cost = quantity * item_price
    
    try:
        # Check 1: Item existence
        if item_name not in INVENTORY:
            raise KeyError(f"Product '{item_name}' does not exist in store catalog.")
        
        # Check 2: Stock availability
        if INVENTORY[item_name] < quantity:
            raise OutOfStockError(f"Only {INVENTORY[item_name]} units available in stock!")
        
        # Check 3: Payment balance
        if wallet_balance < total_cost:
            raise PaymentDeclinedError(f"Order cost ₹{total_cost} exceeds balance ₹{wallet_balance}!")
        
        # Deduct inventory
        INVENTORY[item_name] -= quantity
        order_success = True

    except KeyError as ke:
        print("Catalog Error:", ke)
    except OutOfStockError as ose:
        print("Inventory Error:", ose)
    except PaymentDeclinedError as pde:
        print("Payment Error:", pde)
    else:
        print(f"✅ ORDER CONFIRMED! Charged ₹{total_cost}. Remaining stock: {INVENTORY[item_name]}")
    finally:
        status_label = "SUCCESS" if order_success else "FAILED"
        print(f"[AUDIT LOG] Session finalized with status: {status_label}")

# Run simulated transactions
process_customer_order("Laptop", quantity=1, wallet_balance=65000, item_price=52000)
process_customer_order("Mouse", quantity=1, wallet_balance=5000, item_price=800)
process_customer_order("Headphones", quantity=2, wallet_balance=2000, item_price=2500)
Terminal Output
============================================== 🛒 PROCESSING ORDER: 1x Laptop ============================================== ✅ ORDER CONFIRMED! Charged ₹52000. Remaining stock: 2 [AUDIT LOG] Session finalized with status: SUCCESS ============================================== 🛒 PROCESSING ORDER: 1x Mouse ============================================== Inventory Error: Only 0 units available in stock! [AUDIT LOG] Session finalized with status: FAILED ============================================== 🛒 PROCESSING ORDER: 2x Headphones ============================================== Payment Error: Order cost ₹5000 exceeds balance ₹2000! [AUDIT LOG] Session finalized with status: FAILED
8

University Exam & Viva Preparation Guide (Frequently Asked Questions)

Q1. Differentiate between Syntax Errors and Runtime Errors.
Syntax Errors: Occur when the code violates Python's grammar rules. Detected at compile/parse time before execution starts. Cannot be caught with try...except.
Runtime Errors (Exceptions): Occur while the program is running when an illegal operation is attempted (e.g. dividing by zero). Can be completely caught and handled using try...except.
Q2. What is the role of the `else` block in exception handling?
The else block executes only if no exceptions were raised in the preceding try block. It is used to separate the risky operation inside try from the follow-up code that should run only upon successful completion.
Q3. Does the `finally` block execute if a `return` statement is inside the `try` block?
YES! (The #1 Classic Viva Question) Even if a return, break, or continue statement is encountered inside try or except, Python pauses the return, runs the finally block to completion, and only then executes the return!
Q4. Why is writing a bare `except:` considered bad practice in Python?
A bare except: catches all exceptions indiscriminately, including critical system interrupts like KeyboardInterrupt (Ctrl+C) and SystemExit. This prevents users from terminating stuck programs and masks bugs (e.g. typos in variable names). Always catch specific exceptions!
Q5. How do you trigger an exception manually in Python?
Using the raise keyword followed by the exception class and an optional message. Example: raise ValueError("Marks cannot exceed 100").
Q6. How do you create a custom user-defined exception?
By creating a class that inherits from the built-in Exception class:
class InvalidAgeError(Exception): pass