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.
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.
:) after if/for/def, mismatched parentheses ( ], invalid variable names, or improper indentation (IndentationError).try...except? NO! Because Python halts before executing the script, a try block cannot catch a syntax error in the same file.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.
ZeroDivisionError: Dividing a number by 0 (e.g. 25 / 0).
ValueError: Converting invalid strings to numbers (e.g. int("hello")).
FileNotFoundError: Attempting to open a non-existent file in read mode.
IndexError: Accessing an index outside a list's bounds (e.g. items[99]).
KeyError: Accessing a dictionary key that does not exist.
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.
avg = a + b / 2 instead of (a + b) / 2 (due to operator precedence, division runs before addition!).| 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) |
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.
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!
except blocks completely. Program continues smooth normal execution!try block immediately and transfers control to the matching except handler. Crash is prevented!# 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! 🎉")
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.
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")
| 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. |
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")
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.
age = -15raise ValueError("Age cannot be negative!") triggers immediately.except ValueError: handles rejection cleanly without corrupting the database!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}")
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).
class InsufficientFundsError(Exception): pass# 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)
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.
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)
try...except.try...except.
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.
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!
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!
raise keyword followed by the exception class and an optional message. Example: raise ValueError("Marks cannot exceed 100").
Exception class:
class InvalidAgeError(Exception): pass