Never let your program crash again! Exception Handling is the **superpower** of professional programmers โ it turns ugly runtime crashes into clean, friendly messages, and keeps your program alive no matter what the user throws at it.
> [!TIP]
> **How to use these notes:** Read each section, then type out (don't copy-paste!) the code examples yourself. Pay extra attention to **๐ Board Exam Tips** โ they appear every year. Focus especially on Sections 5.4, 5.4.3, and 5.4.4 โ examiner favourites!
---
## 5.1 Introduction
Picture this: You write a perfect program. It works beautifully in testing. You submit it. Then a user types `"abc"` where a number was expected โ and your whole program **CRASHES** with a red scary error message. ๐ฑ
This is why every professional programmer learns **Exception Handling** โ the art of expecting the unexpected!
::: grid
::: card ๐ฑ | Without Exception Handling | Program crashes the moment anything goes wrong | User sees a confusing traceback and loses all work
::: card ๐ | With Exception Handling | Errors are caught gracefully | User sees a helpful message; program continues or exits cleanly
:::
```
Without Handling: With Handling:
โโโโโโโโโโโโโโโโโ โโโโโโโโโโโโโโโโโ
User enters "abc" User enters "abc"
โ โ
Program CRASHES ๐ฅ Error CAUGHT ๐ฃ
Traceback printed "Please enter a number!"
Program STOPS Program CONTINUES โ
```
> [!NOTE]
> **Real-World Analogy**
> Think of exception handling like a **safety net under a trapeze artist**. The artist (your program) performs amazing moves (operations). If they slip (an error occurs), the safety net (exception handler) catches them โ the show goes on!
---
## 5.2 Exceptions and Exception Handling
### What is an Error?
In programming, things go wrong in **three main ways**. Knowing which type helps you fix it faster:
::: grid
::: card ๐ด | Syntax Error | Mistake in the grammar of Python code | `primt("Hello")` โ typo โ Python won't even start
::: card ๐ | Runtime Error | Valid syntax, but something crashes during execution | `10 / 0` โ can't divide by zero while running
::: card ๐ก | Logical Error | Program runs but gives wrong output | Adding instead of multiplying โ hardest to find!
:::
### What is an Exception?
An **exception** is a **runtime error** that occurs during the execution of a program and disrupts the normal flow of instructions.
When Python hits a problem at runtime, it **raises** (creates) an exception object. If nothing **handles** it, the program crashes with a traceback.
```
Normal Flow: Exception Occurs:
โโโโโโโโโโโโโ โโโโโโโโโโโโโโโโโโ
statement 1 โ statement 1 โ
statement 2 โ statement 2 โ
statement 3 โ statement 3 ๐ฅ โ Exception raised here!
statement 4 โ โ
statement 5 โ Program STOPS (if unhandled)
```
### Code Example: exception_demo.py
```python
# exception_demo.py
# What happens without exception handling?
num = int(input("Enter a number: ")) # User types "abc"
result = 100 / num
print(f"Result: {result}")
# If user types "abc":
# ValueError: invalid literal for int() with base 10: 'abc'
# If user types "0":
# ZeroDivisionError: division by zero
# Program crashes! โ Not professional at all.
```
> [!IMPORTANT]
> **Board Exam Tip โ Key Definitions**
> "Define Exception." โ **1 mark**
> **Answer:** *An exception is an event that occurs during the execution of a program that disrupts the normal flow of the program's instructions. It is a runtime error that Python represents as an object.*
---
## 5.3 Concept of Exception Handling
**Exception Handling** is a mechanism that allows a program to **detect and handle runtime errors gracefully** โ without crashing โ so the program can either recover or exit cleanly.
### The Big Idea: Separate "Try" from "Handle"
```mermaid
flowchart TD
TRY["TRY Block\nRisky Code"]
EXCEPT["EXCEPT Block\nHandle error"]
ELSE["ELSE Block\nIf no error"]
FINALLY["FINALLY Block\nAlways runs (Clean-up)"]
NEXT["Program Continues\nNo crashing!"]
TRY -->|Error?| EXCEPT
TRY -->|No Error?| ELSE
EXCEPT --> FINALLY
ELSE --> FINALLY
FINALLY --> NEXT
classDef tryNode fill:none;
classDef exceptNode fill:none;
classDef elseNode fill:none;
classDef normalNode fill:none;
class TRY tryNode;
class EXCEPT exceptNode;
class ELSE elseNode;
class FINALLY normalNode;
class NEXT normalNode;
```
### Why is this powerful?
::: grid
::: card ๐ก๏ธ | Prevents Crashes | Program survives bad input or missing files | ATM doesn't crash if you type wrong PIN
::: card ๐ฌ | Friendly Messages | Show helpful errors instead of scary tracebacks | "File not found!" instead of FileNotFoundError
::: card ๐ | Program Continues | Handle and move on โ don't lose all progress | Game doesn't quit on one wrong input
::: card ๐งน | Clean Cleanup | Always release resources (files, connections) | Files get closed even if errors occur
:::
> [!NOTE]
> **Exception Handling vs. Error Prevention**
> Exception handling doesn't mean you write buggy code on purpose! Think of it as a **second line of defence**. Write clean code first. Add exception handling for things you can't control โ user input, file operations, network calls, division operations.
---
## 5.4 Exception Handling in Python
Python provides a clean, readable syntax for exception handling using **four keywords**:
```
try โ "Attempt this risky code"
except โ "If that specific error occurs, do this"
else โ "If NO error occurred, also do this"
finally โ "Do this NO MATTER WHAT (error or not)"
```
### Basic Syntax:
```
try:
# Risky code that might raise an exception
except ExceptionType:
# Code to handle the specific exception
else:
# Runs ONLY if no exception was raised in try
finally:
# Runs ALWAYS โ error or no error
```
> [!NOTE]
> `else` and `finally` are optional. The minimum valid structure is just `try` + `except`.
### Code Example: basic_try_except.py
```python
# basic_try_except.py
# Your first exception handler!
try:
num = int(input("Enter a number: "))
result = 100 / num
print(f"100 รท {num} = {result}")
except ZeroDivisionError:
print("โ Oops! Cannot divide by zero. Please enter a non-zero number.")
except ValueError:
print("โ That's not a valid number! Please enter digits only.")
# If user enters 0:
# โ Oops! Cannot divide by zero. Please enter a non-zero number.
# If user enters "abc":
# โ That's not a valid number! Please enter digits only.
# If user enters 4:
# 100 รท 4 = 25.0 โ Works perfectly! โ
```
> [!IMPORTANT]
> **Board Exam Tip**
> "Write the syntax of try-except block in Python." โ **2 marks.**
> Always include `try:` followed by the risky code, then `except ExceptionType:` with the handler. Indentation is mandatory!
---
### 5.4.1 General Built-in Python Exceptions
Python has a **hierarchy of built-in exceptions**. Here are the most important ones for your board exam โ you MUST know these!
```
BaseException
โโโ Exception
โโโ ArithmeticError
โ โโโ ZeroDivisionError โ รท by zero
โโโ LookupError
โ โโโ IndexError โ list[99] out of range
โ โโโ KeyError โ dict["missing_key"]
โโโ TypeError โ wrong data type
โโโ ValueError โ right type, wrong value
โโโ NameError โ variable not defined
โโโ FileNotFoundError โ file doesn't exist
โโโ ImportError โ module not found
โโโ AttributeError โ object has no such attribute
โโโ OverflowError โ number too large
```
**The Must-Know Exceptions โ CBSE Favourites:**
| Exception | When Does It Occur? | Example That Causes It |
| :--- | :--- | :--- |
| `ZeroDivisionError` | Dividing by zero | `10 / 0` |
| `ValueError` | Correct type, invalid value | `int("abc")` |
| `TypeError` | Wrong data type in operation | `"2" + 2` |
| `NameError` | Variable used before assignment | `print(xyz)` (xyz undefined) |
| `IndexError` | List index out of range | `lst[10]` when list has 5 items |
| `KeyError` | Dictionary key doesn't exist | `d["age"]` when key absent |
| `FileNotFoundError` | File not found on disk | `open("ghost.txt")` |
| `ImportError` | Module doesn't exist | `import unicorn` |
| `AttributeError` | Object has no such attribute | `"hello".push("!")` |
| `OverflowError` | Numeric result too large | `math.exp(1000)` |
### Code Example: exception_showcase.py
```python
# exception_showcase.py
# See each exception in action!
# 1. ZeroDivisionError
try:
x = 10 / 0
except ZeroDivisionError:
print("ZeroDivisionError: Can't divide by zero!")
# 2. ValueError
try:
n = int("hello")
except ValueError:
print("ValueError: 'hello' cannot be converted to int!")
# 3. TypeError
try:
result = "5" + 5
except TypeError:
print("TypeError: Can't add string and integer!")
# 4. NameError
try:
print(undefined_var)
except NameError:
print("NameError: Variable not defined!")
# 5. IndexError
try:
lst = [1, 2, 3]
print(lst[10])
except IndexError:
print("IndexError: Index 10 doesn't exist in list of 3!")
# 6. KeyError
try:
d = {"name": "Arjun"}
print(d["age"])
except KeyError:
print("KeyError: 'age' key doesn't exist in dictionary!")
# 7. FileNotFoundError
try:
f = open("marks.txt")
except FileNotFoundError:
print("FileNotFoundError: marks.txt doesn't exist!")
```
> [!IMPORTANT]
> **Board Exam Tip**
> "Name any four built-in exceptions in Python with one example each." โ **4 marks (ยฝ + ยฝ ร 4 = 4).**
> Always pair each exception name with the exact Python statement that triggers it. The table above is your cheat sheet!
> [!TIP]
> **Memory Trick for the Big Five:**
> **"Zero Value Type Name Index"** โ `ZeroDivisionError`, `ValueError`, `TypeError`, `NameError`, `IndexError`
> These are the 5 most commonly tested exceptions!
---
### 5.4.2 Second Argument of the `except` Block
You can **capture the exception object** using `as` keyword in the `except` clause. This gives you access to the actual error message Python generated โ incredibly useful for debugging!
```
Syntax:
except ExceptionType as variable_name:
โ
This captures the exception object
Now you can print its message!
```
### Code Example: except_as_demo.py
```python
# except_as_demo.py
# Catching the exception message itself
try:
num = int(input("Enter a number: "))
result = 100 / num
print(f"Result: {result}")
except ZeroDivisionError as e:
print(f"โ Math Error: {e}")
# Output: โ Math Error: division by zero
except ValueError as e:
print(f"โ Input Error: {e}")
# Output: โ Input Error: invalid literal for int() with base 10: 'abc'
# 'e' is the exception object โ str(e) gives its message!
```
### Code Example: exception_info.py
```python
# exception_info.py
# Getting more info from the exception object
try:
marks = [90, 85, 78]
print(marks[10]) # IndexError
except IndexError as err:
print(f"Error Type : {type(err).__name__}") # IndexError
print(f"Error Message : {err}") # list index out of range
print(f"Handled! Program continues... โ ")
# Output:
# Error Type : IndexError
# Error Message : list index out of range
# Handled! Program continues... โ
```
> [!IMPORTANT]
> **Board Exam Tip**
> "What is the use of `as` keyword in an `except` clause?" โ **2 marks.**
> **Answer:** *The `as` keyword in the `except` clause captures the exception object into a variable. This variable can then be used to access and display the actual error message generated by Python, which is helpful for debugging.*
> [!NOTE]
> **`str(e)` vs `repr(e)`**
> `str(e)` gives a human-readable message: `"division by zero"`
> `repr(e)` gives the full technical representation: `"ZeroDivisionError('division by zero')"`
> For display to users, always use `str(e)` or just `e` in an f-string.
---
### 5.4.3 Handling Multiple Errors
Real programs can fail in multiple ways. Python lets you handle each type of error differently โ or group them together.
#### Method 1: Multiple `except` Clauses (Most Common)
Write one `except` block for each exception type. Python checks them **top to bottom** and executes the FIRST matching one.
```
try:
risky code
except ExceptionType1:
handle error 1
except ExceptionType2:
handle error 2
except ExceptionType3:
handle error 3
```
### Code Example: multiple_except.py
```python
# multiple_except.py
def safe_divide(a, b):
"""Safely divides a by b with multiple error handlers"""
try:
a = int(a) # Might raise ValueError
b = int(b) # Might raise ValueError
result = a / b # Might raise ZeroDivisionError
return result
except ValueError as e:
print(f"โ ValueError: {e}")
print(" โ Please enter numbers only!")
return None
except ZeroDivisionError:
print("โ ZeroDivisionError: Cannot divide by zero!")
print(" โ Please enter a non-zero divisor!")
return None
except TypeError as e:
print(f"โ TypeError: {e}")
return None
# Test cases
print(safe_divide(10, 2)) # 5.0 โ
print(safe_divide(10, 0)) # ZeroDivisionError โ
print(safe_divide("abc", 5)) # ValueError โ
```
#### Method 2: Catching Multiple Exceptions in One `except` (Tuple Syntax)
When two or more exceptions need the **same handling**, list them as a tuple:
```python
except (ZeroDivisionError, ValueError):
print("Either a zero division or bad value occurred!")
```
### Code Example: tuple_except.py
```python
# tuple_except.py
# Handle multiple exceptions the same way
def get_list_item(lst, index):
try:
index = int(index) # ValueError if index is "abc"
return lst[index] # IndexError if out of range
except (ValueError, IndexError) as e:
# Both errors handled identically
print(f"โ Cannot access item: {e}")
return None
nums = [10, 20, 30, 40, 50]
print(get_list_item(nums, 2)) # 30 โ
print(get_list_item(nums, "abc")) # ValueError caught โ
print(get_list_item(nums, 99)) # IndexError caught โ
```
#### Method 3: Bare `except` โ Catch ALL Exceptions
A `except` clause **without specifying any exception type** catches every possible exception. Use this as a last resort!
### Code Example: bare_except.py
```python
# bare_except.py
# Catch-all handler
try:
x = int(input("Enter value: "))
result = 100 / x
print(f"Result: {result}")
except ZeroDivisionError:
print("Cannot divide by zero!") # Specific handler first
except ValueError:
print("Not a valid number!") # Specific handler second
except:
# This catches ANYTHING else that goes wrong
print("โ ๏ธ An unexpected error occurred! Please contact support.")
```
> [!WARNING]
> **Use Bare `except` Cautiously!**
> A bare `except:` hides bugs by catching **everything** โ including keyboard interrupts (`Ctrl+C`), system exits, and programming errors you should fix. Always prefer specific exception types. Use bare `except:` only as a final safety net.
#### Method 4: Using the Base Class `Exception`
```python
try:
risky_code()
except Exception as e:
print(f"Something went wrong: {e}")
```
This catches all non-system exceptions (anything that inherits from `Exception`). A cleaner alternative to bare `except:`.
> [!IMPORTANT]
> **Board Exam Tip โ Order Matters!**
> Always put **specific exceptions BEFORE general ones.**
> ```python
> # โ Correct โ specific first
> except ZeroDivisionError: ...
> except Exception: ...
>
> # โ Wrong โ general first blocks specific ones!
> except Exception: ... # catches everything!
> except ZeroDivisionError: ... # NEVER reached!
> ```
> This is a **guaranteed error-spotting question!**
---
#### The `try-else` Clause
Python's `try` block has an **optional `else`** โ this runs only if the `try` block completed **without any exception**. Think of it as "the success path."
```
try:
risky code
except SomeError:
handle error
else:
# โ This runs ONLY if try succeeded (no exception)
success code here
```
### Code Example: try_else.py
```python
# try_else.py
# The else clause โ for when everything goes right!
def divide_numbers():
try:
a = int(input("Dividend: "))
b = int(input("Divisor : "))
result = a / b
except ZeroDivisionError:
print("โ Cannot divide by zero!")
except ValueError:
print("โ Please enter integers only!")
else:
# Only reaches here if NO exception occurred
print(f"โ Success! {a} รท {b} = {result:.2f}")
print(" (This only prints when everything worked!)")
divide_numbers()
# If b = 0: โ "โ Cannot divide by zero!" (else skipped)
# If a = "xyz": โ "โ Please enter integers only!" (else skipped)
# If a=10, b=4: โ "โ Success! 10 รท 4 = 2.50"
```
> [!TIP]
> **Why use `else` instead of putting code in `try`?**
> Putting success-only code in `else` (not in `try`) is cleaner and safer โ you're explicitly saying "this code should only run if everything above worked." It also prevents accidentally catching errors from the success code itself.
**Complete Exception Handling Flow:**
```
โโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโ
โ COMPLETE FLOW โ
โ โ
โ try: โ
โ โ
โ โ No error? โ Error occurs? โ
โ โ
โ else: except Type1: โ
โ โ
โ except Type2: โ
โ โ
โ except: โ
โ โ
โ โ
โ finally: โโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโ โ
โ โ
โโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโ
```
> [!IMPORTANT]
> **Board Exam Tip**
> "What is the role of `else` clause in a `try-except` block?" โ **2 marks.**
> **Answer:** *The `else` clause in a `try-except` block is executed only when no exception is raised in the `try` block. It is used to write code that should run only upon successful execution of the `try` block. It is not executed if an exception occurs.*
---
### 5.4.4 The `finally` Block
The `finally` block is the **guarantee** of Python exception handling. It **always executes** โ whether an exception occurred or not, whether it was handled or not. Nothing can stop it!
```
try:
except:
finally:
```
- **๐ Scenario 1 (No Error)**
* `try` โ
* โ `except` (skipped)
* โ `finally` โ RUNS
* โ **program continues**
- **๐ Scenario 2 (Error Handled)**
* `try` ๐ฅ
* โ `except` โ
* โ `finally` โ RUNS
* โ **program continues**
- **๐ Scenario 3 (Unhandled Error)**
* `try` ๐ฅ
* โ `except` (doesn't match)
* โ `finally` โ RUNS โ *Still runs!*
* โ **program crashes** (after finally)
### Code Example: finally_demo.py
```python
# finally_demo.py
# finally ALWAYS runs!
def read_file(filename):
f = None
try:
print(f"๐ Opening {filename}...")
f = open(filename, 'r')
data = f.read()
print(f"๐ File contents:\n{data}")
except FileNotFoundError:
print(f"โ File '{filename}' not found!")
except PermissionError:
print(f"โ No permission to read '{filename}'!")
finally:
# This ALWAYS runs โ ensures file is properly closed
if f:
f.close()
print("๐ File closed safely. (finally block ran)")
else:
print("๐ No file to close. (finally block still ran)")
# Test 1: File exists
read_file("grades.txt")
# ๐ Opening grades.txt...
# ๐ File contents: ...
# ๐ File closed safely. (finally block ran)
# Test 2: File doesn't exist
read_file("ghost.txt")
# ๐ Opening ghost.txt...
# โ File 'ghost.txt' not found!
# ๐ No file to close. (finally block still ran)
```
### Code Example: finally_always_runs.py
```python
# finally_always_runs.py
# Demonstrating finally in ALL scenarios
def demonstrate(value):
print(f"\n--- Testing with value: {value} ---")
try:
result = 10 / value
print(f"Result: {result}")
except ZeroDivisionError:
print("Caught: ZeroDivisionError")
finally:
print("โ finally block executed! โ ")
demonstrate(2) # No error โ finally still runs
demonstrate(0) # ZeroDivisionError โ finally still runs
demonstrate("x") # TypeError (unhandled) โ finally still runs, THEN program crashes
# Output:
# --- Testing with value: 2 ---
# Result: 5.0
# โ finally block executed! โ
# --- Testing with value: 0 ---
# Caught: ZeroDivisionError
# โ finally block executed! โ
# --- Testing with value: x ---
# โ finally block executed! โ
# TypeError: unsupported operand type(s) for /: 'int' and 'str'
```
**Real-World Use Cases for `finally`:**
| Scenario | What `finally` Guarantees |
| :--- | :--- |
| File operations | File is **always closed**, even on error |
| Database connections | Connection is **always closed**, preventing leaks |
| Network sockets | Socket is **always disconnected** properly |
| Lock/mutex | Lock is **always released**, preventing deadlock |
| Temporary files | Temp files are **always deleted** |
> [!IMPORTANT]
> **Board Exam Tip โ Most Tested finally Question!**
> "What is the purpose of `finally` block in exception handling?" โ **2 marks.**
> **Answer:** *The `finally` block in Python's exception handling is guaranteed to execute whether an exception is raised or not. It is typically used to perform cleanup operations such as closing files, releasing resources, or closing database connections, ensuring that these actions are always performed regardless of whether an error occurred.*
> [!WARNING]
> **`finally` with `return`**
> If both `try` and `finally` have `return` statements, `finally`'s `return` **overrides** the `try` block's return. This is a tricky board question!
> ```python
> def tricky():
> try:
> return "from try"
> finally:
> return "from finally" # This wins!
>
> print(tricky()) # "from finally"
> ```
---
### 5.4.5 Raising Exceptions โ The `raise` Statement
So far, Python raised exceptions for us. But YOU can also **manually raise an exception** using the `raise` keyword! This is used to enforce rules in your own functions.
```
Syntax:
raise ExceptionType("Your custom error message")
```
### Code Example: raise_demo.py
```python
# raise_demo.py
# Raising exceptions manually
def set_age(age):
"""Sets age โ raises exception if invalid"""
if type(age) != int:
raise TypeError("Age must be an integer!")
if age < 0:
raise ValueError("Age cannot be negative!")
if age > 150:
raise ValueError(f"Age {age} is unrealistically large!")
print(f"โ Age set to: {age}")
# Valid call
set_age(17) # โ Age set to: 17
# Invalid calls โ exceptions raised manually
try:
set_age(-5)
except ValueError as e:
print(f"โ ValueError: {e}") # Age cannot be negative!
try:
set_age(200)
except ValueError as e:
print(f"โ ValueError: {e}") # Age 200 is unrealistically large!
try:
set_age("seventeen")
except TypeError as e:
print(f"โ TypeError: {e}") # Age must be an integer!
```
### Code Example: raise_in_function.py
```python
# raise_in_function.py
# Using raise to enforce business rules
def calculate_discount(price, discount_percent):
"""Calculates discounted price with validation"""
if price < 0:
raise ValueError("Price cannot be negative!")
if not (0 <= discount_percent <= 100):
raise ValueError("Discount must be between 0 and 100!")
discounted = price * (1 - discount_percent / 100)
return round(discounted, 2)
# Safe calls with exception handling
try:
print(calculate_discount(1000, 20)) # โ 800.0
print(calculate_discount(-500, 10)) # โ ValueError
except ValueError as e:
print(f"Invalid input: {e}")
try:
print(calculate_discount(500, 110)) # โ ValueError
except ValueError as e:
print(f"Invalid input: {e}")
```
### `re-raise` โ Re-Raising a Caught Exception
You can re-raise an exception you caught (to log it and then let it propagate) using bare `raise`:
```python
try:
result = 10 / 0
except ZeroDivisionError as e:
print(f"Logging error: {e}") # Log it first
raise # Then re-raise โ let it crash!
```
> [!IMPORTANT]
> **Board Exam Tip**
> "What is the use of `raise` statement in Python?" โ **2 marks.**
> **Answer:** *The `raise` statement in Python is used to manually raise (trigger) an exception. It allows a programmer to enforce custom validation rules and generate meaningful error messages by raising built-in exceptions (like `ValueError`, `TypeError`) with custom error messages when input conditions are not met.*
---
## โ๏ธ Putting It All Together โ Complete Exception Handling Structure
### Code Example: complete_handler.py
```python
# complete_handler.py
# Full try-except-else-finally in one real program
def student_grade_calculator():
"""Calculates student grade from marks input"""
print("=" * 40)
print(" STUDENT GRADE CALCULATOR")
print("=" * 40)
try:
name = input("Student Name : ")
marks = float(input("Marks (0-100): "))
# Manual raise for business rule
if not (0 <= marks <= 100):
raise ValueError(f"Marks must be 0-100, got {marks}")
# Grade calculation
if marks >= 90: grade = 'A+'
elif marks >= 80: grade = 'A'
elif marks >= 70: grade = 'B+'
elif marks >= 60: grade = 'B'
elif marks >= 33: grade = 'C'
else: grade = 'FAIL'
except ValueError as e:
print(f"\nโ Invalid Input: {e}")
print(" Please run the program again.")
grade = None
except KeyboardInterrupt:
print("\n\nโ ๏ธ Program interrupted by user!")
grade = None
else:
# Only runs if try succeeded completely
print(f"\nโ Result for {name}:")
print(f" Marks : {marks}")
print(f" Grade : {grade}")
finally:
# ALWAYS runs
print("\n" + "=" * 40)
print(" Thank you for using Grade Calculator!")
print("=" * 40)
student_grade_calculator()
```
---
## โ ๏ธ Common Errors and Exception Handling Mistakes
| Mistake | Wrong Code โ | Correct Code โ | Explanation |
| :--- | :--- | :--- | :--- |
| General before specific | `except Exception:` then `except ValueError:` | Specific first | ValueError never reached |
| Swallowing exceptions silently | `except: pass` | `except Exception as e: print(e)` | Silent failures are hard to debug |
| Using bare `except` everywhere | `except:` for everything | Use specific types | Hides bugs and system signals |
| No `finally` for resources | File opened, no close | Always use `finally` or `with` | Resource leak! |
| `raise` without message | `raise ValueError` | `raise ValueError("Reason!")` | Always explain WHY |
| Forgetting `global` in handler | Modifying global var | Use `global` keyword | UnboundLocalError |
---
## ๐ Quick Revision โ Exam Ready!
**The Four Keywords:**
- `try` โ Put risky code here
- `except` โ Handle specific errors here
- `else` โ Runs only when try succeeds (no error)
- `finally` โ Runs ALWAYS (cleanup code here)
**Exception Hierarchy โ Must Knows:**
| Exception | Trigger |
| :--- | :--- |
| `ZeroDivisionError` | `x / 0` |
| `ValueError` | `int("abc")` |
| `TypeError` | `"a" + 1` |
| `NameError` | `print(undefined)` |
| `IndexError` | `lst[100]` on short list |
| `KeyError` | `dict["missing"]` |
| `FileNotFoundError` | `open("nope.txt")` |
**`as` keyword:** `except ValueError as e:` โ captures the error object; `print(e)` shows its message.
**Multiple Exceptions:**
```python
except (TypeError, ValueError): # Same handler for both
except TypeError: ...
except ValueError: ... # Different handlers
except Exception as e: ... # Catch-all (use carefully)
```
**`raise` keyword:** Manually trigger exceptions to enforce your own rules.
```python
raise ValueError("Age must be positive!")
```
**`finally` Rule:** Always runs. Use for cleanup (close files, connections, etc.)
---
## ๐ฏ Sample Board Exam Questions
### Q1: What will be the output? [2 marks]
```python
try:
a = 10
b = 0
c = a / b
print(c)
except ZeroDivisionError:
print("Cannot divide by zero")
except:
print("Some other error")
finally:
print("Always executed")
```
**Answer:**
```
Cannot divide by zero
Always executed
```
---
### Q2: Find the error and correct it. [2 marks]
```python
try:
n = int(input("Enter: "))
result = 10 / n
except Exception:
print("General error")
except ZeroDivisionError:
print("Division by zero")
```
**Error:** `SyntaxError` โ `except Exception` (general) is placed before `except ZeroDivisionError` (specific). The specific handler will never be reached.
**Corrected:**
```python
try:
n = int(input("Enter: "))
result = 10 / n
except ZeroDivisionError:
print("Division by zero")
except Exception:
print("General error")
```
---
### Q3: What will be the output? [3 marks]
```python
def divide(a, b):
try:
result = a / b
except ZeroDivisionError:
print("Error: division by zero")
return -1
else:
print("Division successful")
return result
finally:
print("Execution complete")
print(divide(10, 2))
print(divide(10, 0))
```
**Answer:**
```
Division successful
Execution complete
5.0
Error: division by zero
Execution complete
-1
```
*(Note: `finally` runs before the `return` value is actually returned.)*
---
### Q4: Write a program. [3 marks]
Write a Python program that asks the user for two numbers and divides them. Handle `ZeroDivisionError`, `ValueError`, and any other exception. Use `finally` to print "Operation Complete."
```python
try:
a = float(input("Enter dividend: "))
b = float(input("Enter divisor : "))
result = a / b
except ZeroDivisionError:
print("โ Error: Cannot divide by zero!")
except ValueError:
print("โ Error: Please enter numeric values only!")
except Exception as e:
print(f"โ Unexpected error: {e}")
else:
print(f"โ Result: {a} รท {b} = {result:.4f}")
finally:
print("\nOperation Complete.")
```
---
### Q5: Predict the output โ tricky! [2 marks]
```python
try:
lst = [1, 2, 3]
print(lst[1])
print(lst[5])
print("Done")
except IndexError as e:
print(f"Caught: {e}")
else:
print("No errors!")
finally:
print("Finally!")
```
**Answer:**
```
2
Caught: list index out of range
Finally!
```
**Explanation:**
- `lst[1]` โ prints `2` โ
- `lst[5]` โ raises `IndexError` โ jumps to `except`
- `"Done"` is **never reached** (exception interrupted try)
- `else` is **skipped** (because an exception occurred)
- `finally` **always runs** โ
---
### Q6: What is the output? [2 marks]
```python
def check(n):
if n < 0:
raise ValueError("Negative not allowed!")
return n * 2
try:
print(check(5))
print(check(-3))
print(check(10))
except ValueError as e:
print(f"Handled: {e}")
```
**Answer:**
```
10
Handled: Negative not allowed!
```
*(After `check(-3)` raises `ValueError`, execution jumps to `except` โ `check(10)` never runs.)*
---
## Practice Problems
Strengthen your skills with these exercises:
1. Write a program that accepts a list from the user and a position. Print the element at that position, handling `IndexError` and `ValueError` appropriately.
2. Write a function `safe_open(filename)` that opens a text file, reads and prints its contents, and uses `finally` to always close the file. Handle `FileNotFoundError`.
3. Write a function `validate_marks(marks)` that uses `raise` to enforce: marks must be int, between 0 and 100. Test it with try-except.
4. Write a program that demonstrates `try-except-else-finally` with all four parts executing at least once (show two runs: one with error, one without).
5. What is the output of the following code? Explain each line.
```python
for i in [2, 0, "x", 5]:
try:
print(10 / int(i))
except ZeroDivisionError:
print("Zero!")
except (ValueError, TypeError):
print("Bad value!")
```
6. Write a program that keeps asking the user for a positive integer until they provide one. Use a `while True` loop with `try-except` to handle bad inputs gracefully.
7. Create a dictionary with 3 student names and marks. Write a program that lets the user look up marks by name, handling `KeyError` if the name is not found.
8. Write a function `convert_to_int(value)` that uses `raise` and `except` to validate that `value` is convertible to an integer, returning the converted value or a helpful error message.
> [!TIP]
> **How to use these notes:** Read each section, then type out (don't copy-paste!) the code examples yourself. Pay extra attention to **๐ Board Exam Tips** โ they appear every year. Focus especially on Sections 5.4, 5.4.3, and 5.4.4 โ examiner favourites!
---
## 5.1 Introduction
Picture this: You write a perfect program. It works beautifully in testing. You submit it. Then a user types `"abc"` where a number was expected โ and your whole program **CRASHES** with a red scary error message. ๐ฑ
This is why every professional programmer learns **Exception Handling** โ the art of expecting the unexpected!
::: grid
::: card ๐ฑ | Without Exception Handling | Program crashes the moment anything goes wrong | User sees a confusing traceback and loses all work
::: card ๐ | With Exception Handling | Errors are caught gracefully | User sees a helpful message; program continues or exits cleanly
:::
```
Without Handling: With Handling:
โโโโโโโโโโโโโโโโโ โโโโโโโโโโโโโโโโโ
User enters "abc" User enters "abc"
โ โ
Program CRASHES ๐ฅ Error CAUGHT ๐ฃ
Traceback printed "Please enter a number!"
Program STOPS Program CONTINUES โ
```
> [!NOTE]
> **Real-World Analogy**
> Think of exception handling like a **safety net under a trapeze artist**. The artist (your program) performs amazing moves (operations). If they slip (an error occurs), the safety net (exception handler) catches them โ the show goes on!
---
## 5.2 Exceptions and Exception Handling
### What is an Error?
In programming, things go wrong in **three main ways**. Knowing which type helps you fix it faster:
::: grid
::: card ๐ด | Syntax Error | Mistake in the grammar of Python code | `primt("Hello")` โ typo โ Python won't even start
::: card ๐ | Runtime Error | Valid syntax, but something crashes during execution | `10 / 0` โ can't divide by zero while running
::: card ๐ก | Logical Error | Program runs but gives wrong output | Adding instead of multiplying โ hardest to find!
:::
### What is an Exception?
An **exception** is a **runtime error** that occurs during the execution of a program and disrupts the normal flow of instructions.
When Python hits a problem at runtime, it **raises** (creates) an exception object. If nothing **handles** it, the program crashes with a traceback.
```
Normal Flow: Exception Occurs:
โโโโโโโโโโโโโ โโโโโโโโโโโโโโโโโโ
statement 1 โ statement 1 โ
statement 2 โ statement 2 โ
statement 3 โ statement 3 ๐ฅ โ Exception raised here!
statement 4 โ โ
statement 5 โ Program STOPS (if unhandled)
```
### Code Example: exception_demo.py
```python
# exception_demo.py
# What happens without exception handling?
num = int(input("Enter a number: ")) # User types "abc"
result = 100 / num
print(f"Result: {result}")
# If user types "abc":
# ValueError: invalid literal for int() with base 10: 'abc'
# If user types "0":
# ZeroDivisionError: division by zero
# Program crashes! โ Not professional at all.
```
> [!IMPORTANT]
> **Board Exam Tip โ Key Definitions**
> "Define Exception." โ **1 mark**
> **Answer:** *An exception is an event that occurs during the execution of a program that disrupts the normal flow of the program's instructions. It is a runtime error that Python represents as an object.*
---
## 5.3 Concept of Exception Handling
**Exception Handling** is a mechanism that allows a program to **detect and handle runtime errors gracefully** โ without crashing โ so the program can either recover or exit cleanly.
### The Big Idea: Separate "Try" from "Handle"
```mermaid
flowchart TD
TRY["TRY Block\nRisky Code"]
EXCEPT["EXCEPT Block\nHandle error"]
ELSE["ELSE Block\nIf no error"]
FINALLY["FINALLY Block\nAlways runs (Clean-up)"]
NEXT["Program Continues\nNo crashing!"]
TRY -->|Error?| EXCEPT
TRY -->|No Error?| ELSE
EXCEPT --> FINALLY
ELSE --> FINALLY
FINALLY --> NEXT
classDef tryNode fill:none;
classDef exceptNode fill:none;
classDef elseNode fill:none;
classDef normalNode fill:none;
class TRY tryNode;
class EXCEPT exceptNode;
class ELSE elseNode;
class FINALLY normalNode;
class NEXT normalNode;
```
### Why is this powerful?
::: grid
::: card ๐ก๏ธ | Prevents Crashes | Program survives bad input or missing files | ATM doesn't crash if you type wrong PIN
::: card ๐ฌ | Friendly Messages | Show helpful errors instead of scary tracebacks | "File not found!" instead of FileNotFoundError
::: card ๐ | Program Continues | Handle and move on โ don't lose all progress | Game doesn't quit on one wrong input
::: card ๐งน | Clean Cleanup | Always release resources (files, connections) | Files get closed even if errors occur
:::
> [!NOTE]
> **Exception Handling vs. Error Prevention**
> Exception handling doesn't mean you write buggy code on purpose! Think of it as a **second line of defence**. Write clean code first. Add exception handling for things you can't control โ user input, file operations, network calls, division operations.
---
## 5.4 Exception Handling in Python
Python provides a clean, readable syntax for exception handling using **four keywords**:
```
try โ "Attempt this risky code"
except โ "If that specific error occurs, do this"
else โ "If NO error occurred, also do this"
finally โ "Do this NO MATTER WHAT (error or not)"
```
### Basic Syntax:
```
try:
# Risky code that might raise an exception
except ExceptionType:
# Code to handle the specific exception
else:
# Runs ONLY if no exception was raised in try
finally:
# Runs ALWAYS โ error or no error
```
> [!NOTE]
> `else` and `finally` are optional. The minimum valid structure is just `try` + `except`.
### Code Example: basic_try_except.py
```python
# basic_try_except.py
# Your first exception handler!
try:
num = int(input("Enter a number: "))
result = 100 / num
print(f"100 รท {num} = {result}")
except ZeroDivisionError:
print("โ Oops! Cannot divide by zero. Please enter a non-zero number.")
except ValueError:
print("โ That's not a valid number! Please enter digits only.")
# If user enters 0:
# โ Oops! Cannot divide by zero. Please enter a non-zero number.
# If user enters "abc":
# โ That's not a valid number! Please enter digits only.
# If user enters 4:
# 100 รท 4 = 25.0 โ Works perfectly! โ
```
> [!IMPORTANT]
> **Board Exam Tip**
> "Write the syntax of try-except block in Python." โ **2 marks.**
> Always include `try:` followed by the risky code, then `except ExceptionType:` with the handler. Indentation is mandatory!
---
### 5.4.1 General Built-in Python Exceptions
Python has a **hierarchy of built-in exceptions**. Here are the most important ones for your board exam โ you MUST know these!
```
BaseException
โโโ Exception
โโโ ArithmeticError
โ โโโ ZeroDivisionError โ รท by zero
โโโ LookupError
โ โโโ IndexError โ list[99] out of range
โ โโโ KeyError โ dict["missing_key"]
โโโ TypeError โ wrong data type
โโโ ValueError โ right type, wrong value
โโโ NameError โ variable not defined
โโโ FileNotFoundError โ file doesn't exist
โโโ ImportError โ module not found
โโโ AttributeError โ object has no such attribute
โโโ OverflowError โ number too large
```
**The Must-Know Exceptions โ CBSE Favourites:**
| Exception | When Does It Occur? | Example That Causes It |
| :--- | :--- | :--- |
| `ZeroDivisionError` | Dividing by zero | `10 / 0` |
| `ValueError` | Correct type, invalid value | `int("abc")` |
| `TypeError` | Wrong data type in operation | `"2" + 2` |
| `NameError` | Variable used before assignment | `print(xyz)` (xyz undefined) |
| `IndexError` | List index out of range | `lst[10]` when list has 5 items |
| `KeyError` | Dictionary key doesn't exist | `d["age"]` when key absent |
| `FileNotFoundError` | File not found on disk | `open("ghost.txt")` |
| `ImportError` | Module doesn't exist | `import unicorn` |
| `AttributeError` | Object has no such attribute | `"hello".push("!")` |
| `OverflowError` | Numeric result too large | `math.exp(1000)` |
### Code Example: exception_showcase.py
```python
# exception_showcase.py
# See each exception in action!
# 1. ZeroDivisionError
try:
x = 10 / 0
except ZeroDivisionError:
print("ZeroDivisionError: Can't divide by zero!")
# 2. ValueError
try:
n = int("hello")
except ValueError:
print("ValueError: 'hello' cannot be converted to int!")
# 3. TypeError
try:
result = "5" + 5
except TypeError:
print("TypeError: Can't add string and integer!")
# 4. NameError
try:
print(undefined_var)
except NameError:
print("NameError: Variable not defined!")
# 5. IndexError
try:
lst = [1, 2, 3]
print(lst[10])
except IndexError:
print("IndexError: Index 10 doesn't exist in list of 3!")
# 6. KeyError
try:
d = {"name": "Arjun"}
print(d["age"])
except KeyError:
print("KeyError: 'age' key doesn't exist in dictionary!")
# 7. FileNotFoundError
try:
f = open("marks.txt")
except FileNotFoundError:
print("FileNotFoundError: marks.txt doesn't exist!")
```
> [!IMPORTANT]
> **Board Exam Tip**
> "Name any four built-in exceptions in Python with one example each." โ **4 marks (ยฝ + ยฝ ร 4 = 4).**
> Always pair each exception name with the exact Python statement that triggers it. The table above is your cheat sheet!
> [!TIP]
> **Memory Trick for the Big Five:**
> **"Zero Value Type Name Index"** โ `ZeroDivisionError`, `ValueError`, `TypeError`, `NameError`, `IndexError`
> These are the 5 most commonly tested exceptions!
---
### 5.4.2 Second Argument of the `except` Block
You can **capture the exception object** using `as` keyword in the `except` clause. This gives you access to the actual error message Python generated โ incredibly useful for debugging!
```
Syntax:
except ExceptionType as variable_name:
โ
This captures the exception object
Now you can print its message!
```
### Code Example: except_as_demo.py
```python
# except_as_demo.py
# Catching the exception message itself
try:
num = int(input("Enter a number: "))
result = 100 / num
print(f"Result: {result}")
except ZeroDivisionError as e:
print(f"โ Math Error: {e}")
# Output: โ Math Error: division by zero
except ValueError as e:
print(f"โ Input Error: {e}")
# Output: โ Input Error: invalid literal for int() with base 10: 'abc'
# 'e' is the exception object โ str(e) gives its message!
```
### Code Example: exception_info.py
```python
# exception_info.py
# Getting more info from the exception object
try:
marks = [90, 85, 78]
print(marks[10]) # IndexError
except IndexError as err:
print(f"Error Type : {type(err).__name__}") # IndexError
print(f"Error Message : {err}") # list index out of range
print(f"Handled! Program continues... โ ")
# Output:
# Error Type : IndexError
# Error Message : list index out of range
# Handled! Program continues... โ
```
> [!IMPORTANT]
> **Board Exam Tip**
> "What is the use of `as` keyword in an `except` clause?" โ **2 marks.**
> **Answer:** *The `as` keyword in the `except` clause captures the exception object into a variable. This variable can then be used to access and display the actual error message generated by Python, which is helpful for debugging.*
> [!NOTE]
> **`str(e)` vs `repr(e)`**
> `str(e)` gives a human-readable message: `"division by zero"`
> `repr(e)` gives the full technical representation: `"ZeroDivisionError('division by zero')"`
> For display to users, always use `str(e)` or just `e` in an f-string.
---
### 5.4.3 Handling Multiple Errors
Real programs can fail in multiple ways. Python lets you handle each type of error differently โ or group them together.
#### Method 1: Multiple `except` Clauses (Most Common)
Write one `except` block for each exception type. Python checks them **top to bottom** and executes the FIRST matching one.
```
try:
risky code
except ExceptionType1:
handle error 1
except ExceptionType2:
handle error 2
except ExceptionType3:
handle error 3
```
### Code Example: multiple_except.py
```python
# multiple_except.py
def safe_divide(a, b):
"""Safely divides a by b with multiple error handlers"""
try:
a = int(a) # Might raise ValueError
b = int(b) # Might raise ValueError
result = a / b # Might raise ZeroDivisionError
return result
except ValueError as e:
print(f"โ ValueError: {e}")
print(" โ Please enter numbers only!")
return None
except ZeroDivisionError:
print("โ ZeroDivisionError: Cannot divide by zero!")
print(" โ Please enter a non-zero divisor!")
return None
except TypeError as e:
print(f"โ TypeError: {e}")
return None
# Test cases
print(safe_divide(10, 2)) # 5.0 โ
print(safe_divide(10, 0)) # ZeroDivisionError โ
print(safe_divide("abc", 5)) # ValueError โ
```
#### Method 2: Catching Multiple Exceptions in One `except` (Tuple Syntax)
When two or more exceptions need the **same handling**, list them as a tuple:
```python
except (ZeroDivisionError, ValueError):
print("Either a zero division or bad value occurred!")
```
### Code Example: tuple_except.py
```python
# tuple_except.py
# Handle multiple exceptions the same way
def get_list_item(lst, index):
try:
index = int(index) # ValueError if index is "abc"
return lst[index] # IndexError if out of range
except (ValueError, IndexError) as e:
# Both errors handled identically
print(f"โ Cannot access item: {e}")
return None
nums = [10, 20, 30, 40, 50]
print(get_list_item(nums, 2)) # 30 โ
print(get_list_item(nums, "abc")) # ValueError caught โ
print(get_list_item(nums, 99)) # IndexError caught โ
```
#### Method 3: Bare `except` โ Catch ALL Exceptions
A `except` clause **without specifying any exception type** catches every possible exception. Use this as a last resort!
### Code Example: bare_except.py
```python
# bare_except.py
# Catch-all handler
try:
x = int(input("Enter value: "))
result = 100 / x
print(f"Result: {result}")
except ZeroDivisionError:
print("Cannot divide by zero!") # Specific handler first
except ValueError:
print("Not a valid number!") # Specific handler second
except:
# This catches ANYTHING else that goes wrong
print("โ ๏ธ An unexpected error occurred! Please contact support.")
```
> [!WARNING]
> **Use Bare `except` Cautiously!**
> A bare `except:` hides bugs by catching **everything** โ including keyboard interrupts (`Ctrl+C`), system exits, and programming errors you should fix. Always prefer specific exception types. Use bare `except:` only as a final safety net.
#### Method 4: Using the Base Class `Exception`
```python
try:
risky_code()
except Exception as e:
print(f"Something went wrong: {e}")
```
This catches all non-system exceptions (anything that inherits from `Exception`). A cleaner alternative to bare `except:`.
> [!IMPORTANT]
> **Board Exam Tip โ Order Matters!**
> Always put **specific exceptions BEFORE general ones.**
> ```python
> # โ Correct โ specific first
> except ZeroDivisionError: ...
> except Exception: ...
>
> # โ Wrong โ general first blocks specific ones!
> except Exception: ... # catches everything!
> except ZeroDivisionError: ... # NEVER reached!
> ```
> This is a **guaranteed error-spotting question!**
---
#### The `try-else` Clause
Python's `try` block has an **optional `else`** โ this runs only if the `try` block completed **without any exception**. Think of it as "the success path."
```
try:
risky code
except SomeError:
handle error
else:
# โ This runs ONLY if try succeeded (no exception)
success code here
```
### Code Example: try_else.py
```python
# try_else.py
# The else clause โ for when everything goes right!
def divide_numbers():
try:
a = int(input("Dividend: "))
b = int(input("Divisor : "))
result = a / b
except ZeroDivisionError:
print("โ Cannot divide by zero!")
except ValueError:
print("โ Please enter integers only!")
else:
# Only reaches here if NO exception occurred
print(f"โ Success! {a} รท {b} = {result:.2f}")
print(" (This only prints when everything worked!)")
divide_numbers()
# If b = 0: โ "โ Cannot divide by zero!" (else skipped)
# If a = "xyz": โ "โ Please enter integers only!" (else skipped)
# If a=10, b=4: โ "โ Success! 10 รท 4 = 2.50"
```
> [!TIP]
> **Why use `else` instead of putting code in `try`?**
> Putting success-only code in `else` (not in `try`) is cleaner and safer โ you're explicitly saying "this code should only run if everything above worked." It also prevents accidentally catching errors from the success code itself.
**Complete Exception Handling Flow:**
```
โโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโ
โ COMPLETE FLOW โ
โ โ
โ try: โ
โ
โ โ No error? โ Error occurs? โ
โ โ
โ else: except Type1: โ
โ
โ except Type2: โ
โ
โ except: โ
โ
โ โ
โ finally: โโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโ โ
โ
โโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโ
```
> [!IMPORTANT]
> **Board Exam Tip**
> "What is the role of `else` clause in a `try-except` block?" โ **2 marks.**
> **Answer:** *The `else` clause in a `try-except` block is executed only when no exception is raised in the `try` block. It is used to write code that should run only upon successful execution of the `try` block. It is not executed if an exception occurs.*
---
### 5.4.4 The `finally` Block
The `finally` block is the **guarantee** of Python exception handling. It **always executes** โ whether an exception occurred or not, whether it was handled or not. Nothing can stop it!
```
try:
except:
finally:
```
- **๐ Scenario 1 (No Error)**
* `try` โ
* โ `except` (skipped)
* โ `finally` โ RUNS
* โ **program continues**
- **๐ Scenario 2 (Error Handled)**
* `try` ๐ฅ
* โ `except` โ
* โ `finally` โ RUNS
* โ **program continues**
- **๐ Scenario 3 (Unhandled Error)**
* `try` ๐ฅ
* โ `except` (doesn't match)
* โ `finally` โ RUNS โ *Still runs!*
* โ **program crashes** (after finally)
### Code Example: finally_demo.py
```python
# finally_demo.py
# finally ALWAYS runs!
def read_file(filename):
f = None
try:
print(f"๐ Opening {filename}...")
f = open(filename, 'r')
data = f.read()
print(f"๐ File contents:\n{data}")
except FileNotFoundError:
print(f"โ File '{filename}' not found!")
except PermissionError:
print(f"โ No permission to read '{filename}'!")
finally:
# This ALWAYS runs โ ensures file is properly closed
if f:
f.close()
print("๐ File closed safely. (finally block ran)")
else:
print("๐ No file to close. (finally block still ran)")
# Test 1: File exists
read_file("grades.txt")
# ๐ Opening grades.txt...
# ๐ File contents: ...
# ๐ File closed safely. (finally block ran)
# Test 2: File doesn't exist
read_file("ghost.txt")
# ๐ Opening ghost.txt...
# โ File 'ghost.txt' not found!
# ๐ No file to close. (finally block still ran)
```
### Code Example: finally_always_runs.py
```python
# finally_always_runs.py
# Demonstrating finally in ALL scenarios
def demonstrate(value):
print(f"\n--- Testing with value: {value} ---")
try:
result = 10 / value
print(f"Result: {result}")
except ZeroDivisionError:
print("Caught: ZeroDivisionError")
finally:
print("โ finally block executed! โ ")
demonstrate(2) # No error โ finally still runs
demonstrate(0) # ZeroDivisionError โ finally still runs
demonstrate("x") # TypeError (unhandled) โ finally still runs, THEN program crashes
# Output:
# --- Testing with value: 2 ---
# Result: 5.0
# โ finally block executed! โ
# --- Testing with value: 0 ---
# Caught: ZeroDivisionError
# โ finally block executed! โ
# --- Testing with value: x ---
# โ finally block executed! โ
# TypeError: unsupported operand type(s) for /: 'int' and 'str'
```
**Real-World Use Cases for `finally`:**
| Scenario | What `finally` Guarantees |
| :--- | :--- |
| File operations | File is **always closed**, even on error |
| Database connections | Connection is **always closed**, preventing leaks |
| Network sockets | Socket is **always disconnected** properly |
| Lock/mutex | Lock is **always released**, preventing deadlock |
| Temporary files | Temp files are **always deleted** |
> [!IMPORTANT]
> **Board Exam Tip โ Most Tested finally Question!**
> "What is the purpose of `finally` block in exception handling?" โ **2 marks.**
> **Answer:** *The `finally` block in Python's exception handling is guaranteed to execute whether an exception is raised or not. It is typically used to perform cleanup operations such as closing files, releasing resources, or closing database connections, ensuring that these actions are always performed regardless of whether an error occurred.*
> [!WARNING]
> **`finally` with `return`**
> If both `try` and `finally` have `return` statements, `finally`'s `return` **overrides** the `try` block's return. This is a tricky board question!
> ```python
> def tricky():
> try:
> return "from try"
> finally:
> return "from finally" # This wins!
>
> print(tricky()) # "from finally"
> ```
---
### 5.4.5 Raising Exceptions โ The `raise` Statement
So far, Python raised exceptions for us. But YOU can also **manually raise an exception** using the `raise` keyword! This is used to enforce rules in your own functions.
```
Syntax:
raise ExceptionType("Your custom error message")
```
### Code Example: raise_demo.py
```python
# raise_demo.py
# Raising exceptions manually
def set_age(age):
"""Sets age โ raises exception if invalid"""
if type(age) != int:
raise TypeError("Age must be an integer!")
if age < 0:
raise ValueError("Age cannot be negative!")
if age > 150:
raise ValueError(f"Age {age} is unrealistically large!")
print(f"โ Age set to: {age}")
# Valid call
set_age(17) # โ Age set to: 17
# Invalid calls โ exceptions raised manually
try:
set_age(-5)
except ValueError as e:
print(f"โ ValueError: {e}") # Age cannot be negative!
try:
set_age(200)
except ValueError as e:
print(f"โ ValueError: {e}") # Age 200 is unrealistically large!
try:
set_age("seventeen")
except TypeError as e:
print(f"โ TypeError: {e}") # Age must be an integer!
```
### Code Example: raise_in_function.py
```python
# raise_in_function.py
# Using raise to enforce business rules
def calculate_discount(price, discount_percent):
"""Calculates discounted price with validation"""
if price < 0:
raise ValueError("Price cannot be negative!")
if not (0 <= discount_percent <= 100):
raise ValueError("Discount must be between 0 and 100!")
discounted = price * (1 - discount_percent / 100)
return round(discounted, 2)
# Safe calls with exception handling
try:
print(calculate_discount(1000, 20)) # โ 800.0
print(calculate_discount(-500, 10)) # โ ValueError
except ValueError as e:
print(f"Invalid input: {e}")
try:
print(calculate_discount(500, 110)) # โ ValueError
except ValueError as e:
print(f"Invalid input: {e}")
```
### `re-raise` โ Re-Raising a Caught Exception
You can re-raise an exception you caught (to log it and then let it propagate) using bare `raise`:
```python
try:
result = 10 / 0
except ZeroDivisionError as e:
print(f"Logging error: {e}") # Log it first
raise # Then re-raise โ let it crash!
```
> [!IMPORTANT]
> **Board Exam Tip**
> "What is the use of `raise` statement in Python?" โ **2 marks.**
> **Answer:** *The `raise` statement in Python is used to manually raise (trigger) an exception. It allows a programmer to enforce custom validation rules and generate meaningful error messages by raising built-in exceptions (like `ValueError`, `TypeError`) with custom error messages when input conditions are not met.*
---
## โ๏ธ Putting It All Together โ Complete Exception Handling Structure
### Code Example: complete_handler.py
```python
# complete_handler.py
# Full try-except-else-finally in one real program
def student_grade_calculator():
"""Calculates student grade from marks input"""
print("=" * 40)
print(" STUDENT GRADE CALCULATOR")
print("=" * 40)
try:
name = input("Student Name : ")
marks = float(input("Marks (0-100): "))
# Manual raise for business rule
if not (0 <= marks <= 100):
raise ValueError(f"Marks must be 0-100, got {marks}")
# Grade calculation
if marks >= 90: grade = 'A+'
elif marks >= 80: grade = 'A'
elif marks >= 70: grade = 'B+'
elif marks >= 60: grade = 'B'
elif marks >= 33: grade = 'C'
else: grade = 'FAIL'
except ValueError as e:
print(f"\nโ Invalid Input: {e}")
print(" Please run the program again.")
grade = None
except KeyboardInterrupt:
print("\n\nโ ๏ธ Program interrupted by user!")
grade = None
else:
# Only runs if try succeeded completely
print(f"\nโ Result for {name}:")
print(f" Marks : {marks}")
print(f" Grade : {grade}")
finally:
# ALWAYS runs
print("\n" + "=" * 40)
print(" Thank you for using Grade Calculator!")
print("=" * 40)
student_grade_calculator()
```
---
## โ ๏ธ Common Errors and Exception Handling Mistakes
| Mistake | Wrong Code โ | Correct Code โ | Explanation |
| :--- | :--- | :--- | :--- |
| General before specific | `except Exception:` then `except ValueError:` | Specific first | ValueError never reached |
| Swallowing exceptions silently | `except: pass` | `except Exception as e: print(e)` | Silent failures are hard to debug |
| Using bare `except` everywhere | `except:` for everything | Use specific types | Hides bugs and system signals |
| No `finally` for resources | File opened, no close | Always use `finally` or `with` | Resource leak! |
| `raise` without message | `raise ValueError` | `raise ValueError("Reason!")` | Always explain WHY |
| Forgetting `global` in handler | Modifying global var | Use `global` keyword | UnboundLocalError |
---
## ๐ Quick Revision โ Exam Ready!
**The Four Keywords:**
- `try` โ Put risky code here
- `except` โ Handle specific errors here
- `else` โ Runs only when try succeeds (no error)
- `finally` โ Runs ALWAYS (cleanup code here)
**Exception Hierarchy โ Must Knows:**
| Exception | Trigger |
| :--- | :--- |
| `ZeroDivisionError` | `x / 0` |
| `ValueError` | `int("abc")` |
| `TypeError` | `"a" + 1` |
| `NameError` | `print(undefined)` |
| `IndexError` | `lst[100]` on short list |
| `KeyError` | `dict["missing"]` |
| `FileNotFoundError` | `open("nope.txt")` |
**`as` keyword:** `except ValueError as e:` โ captures the error object; `print(e)` shows its message.
**Multiple Exceptions:**
```python
except (TypeError, ValueError): # Same handler for both
except TypeError: ...
except ValueError: ... # Different handlers
except Exception as e: ... # Catch-all (use carefully)
```
**`raise` keyword:** Manually trigger exceptions to enforce your own rules.
```python
raise ValueError("Age must be positive!")
```
**`finally` Rule:** Always runs. Use for cleanup (close files, connections, etc.)
---
## ๐ฏ Sample Board Exam Questions
### Q1: What will be the output? [2 marks]
```python
try:
a = 10
b = 0
c = a / b
print(c)
except ZeroDivisionError:
print("Cannot divide by zero")
except:
print("Some other error")
finally:
print("Always executed")
```
**Answer:**
```
Cannot divide by zero
Always executed
```
---
### Q2: Find the error and correct it. [2 marks]
```python
try:
n = int(input("Enter: "))
result = 10 / n
except Exception:
print("General error")
except ZeroDivisionError:
print("Division by zero")
```
**Error:** `SyntaxError` โ `except Exception` (general) is placed before `except ZeroDivisionError` (specific). The specific handler will never be reached.
**Corrected:**
```python
try:
n = int(input("Enter: "))
result = 10 / n
except ZeroDivisionError:
print("Division by zero")
except Exception:
print("General error")
```
---
### Q3: What will be the output? [3 marks]
```python
def divide(a, b):
try:
result = a / b
except ZeroDivisionError:
print("Error: division by zero")
return -1
else:
print("Division successful")
return result
finally:
print("Execution complete")
print(divide(10, 2))
print(divide(10, 0))
```
**Answer:**
```
Division successful
Execution complete
5.0
Error: division by zero
Execution complete
-1
```
*(Note: `finally` runs before the `return` value is actually returned.)*
---
### Q4: Write a program. [3 marks]
Write a Python program that asks the user for two numbers and divides them. Handle `ZeroDivisionError`, `ValueError`, and any other exception. Use `finally` to print "Operation Complete."
```python
try:
a = float(input("Enter dividend: "))
b = float(input("Enter divisor : "))
result = a / b
except ZeroDivisionError:
print("โ Error: Cannot divide by zero!")
except ValueError:
print("โ Error: Please enter numeric values only!")
except Exception as e:
print(f"โ Unexpected error: {e}")
else:
print(f"โ Result: {a} รท {b} = {result:.4f}")
finally:
print("\nOperation Complete.")
```
---
### Q5: Predict the output โ tricky! [2 marks]
```python
try:
lst = [1, 2, 3]
print(lst[1])
print(lst[5])
print("Done")
except IndexError as e:
print(f"Caught: {e}")
else:
print("No errors!")
finally:
print("Finally!")
```
**Answer:**
```
2
Caught: list index out of range
Finally!
```
**Explanation:**
- `lst[1]` โ prints `2` โ
- `lst[5]` โ raises `IndexError` โ jumps to `except`
- `"Done"` is **never reached** (exception interrupted try)
- `else` is **skipped** (because an exception occurred)
- `finally` **always runs** โ
---
### Q6: What is the output? [2 marks]
```python
def check(n):
if n < 0:
raise ValueError("Negative not allowed!")
return n * 2
try:
print(check(5))
print(check(-3))
print(check(10))
except ValueError as e:
print(f"Handled: {e}")
```
**Answer:**
```
10
Handled: Negative not allowed!
```
*(After `check(-3)` raises `ValueError`, execution jumps to `except` โ `check(10)` never runs.)*
---
## Practice Problems
Strengthen your skills with these exercises:
1. Write a program that accepts a list from the user and a position. Print the element at that position, handling `IndexError` and `ValueError` appropriately.
2. Write a function `safe_open(filename)` that opens a text file, reads and prints its contents, and uses `finally` to always close the file. Handle `FileNotFoundError`.
3. Write a function `validate_marks(marks)` that uses `raise` to enforce: marks must be int, between 0 and 100. Test it with try-except.
4. Write a program that demonstrates `try-except-else-finally` with all four parts executing at least once (show two runs: one with error, one without).
5. What is the output of the following code? Explain each line.
```python
for i in [2, 0, "x", 5]:
try:
print(10 / int(i))
except ZeroDivisionError:
print("Zero!")
except (ValueError, TypeError):
print("Bad value!")
```
6. Write a program that keeps asking the user for a positive integer until they provide one. Use a `while True` loop with `try-except` to handle bad inputs gracefully.
7. Create a dictionary with 3 student names and marks. Write a program that lets the user look up marks by name, handling `KeyError` if the name is not found.
8. Write a function `convert_to_int(value)` that uses `raise` and `except` to validate that `value` is convertible to an integer, returning the converted value or a helpful error message.