Day 10: Exception Handling & Debugging
1. Learning Objectives
By the end of Day 10, you will be able to:
Understand what exceptions are and why they occur
Use
try,except,else, andfinallyto handle errors gracefullyCatch specific exceptions vs. generic ones
Raise your own exceptions with
raiseRead and understand traceback messages to pinpoint bugs
Apply basic debugging techniques (print, assert, logging overview)
Write programs that fail gracefully instead of crashing
2. Concept Explanation
2.1 Why Exception Handling? — Programs Will Fail
No matter how carefully you code, errors happen. Files are missing, users type nonsense, networks go down. Without exception handling, your program crashes. With it, your program can respond gracefully and keep running.
# ❌ Without handling — crashes on bad input
age = int(input("Age: ")) # User types "twenty" → ValueError crash
# ✅ With handling — graceful response
try:
age = int(input("Age: "))
except ValueError:
print("Please enter a valid number.")
2.2 The try / except Block
The fundamental structure:
try:
# Code that might raise an exception
result = 10 / 0
except ZeroDivisionError:
# Runs only if ZeroDivisionError occurs in try block
print("You can't divide by zero!")
How it works:
Python runs the
tryblock.If an exception occurs, Python jumps to the first
exceptthat matches the exception type.If no exception occurs, the
exceptblock is skipped.The program continues after the
try/exceptblock.
2.3 Catching Specific Exceptions (Best Practice)
Never use a bare except — it catches everything, even KeyboardInterrupt (Ctrl+C) and system exits, which you usually want to allow.
# ❌ Bad — catches everything, hides real bugs
try:
risky_code()
except:
pass
# ✅ Good — catches only what you expect
try:
num = int(input("Number: "))
print(10 / num)
except ValueError:
print("That's not an integer!")
except ZeroDivisionError:
print("Cannot divide by zero.")
Common built-in exceptions:
| Exception | When It Occurs |
|---|---|
ValueError |
Bad conversion: int("hello") |
TypeError |
Wrong type: "hello" + 5 |
ZeroDivisionError |
Division by zero: 10 / 0 |
FileNotFoundError |
File doesn't exist: open("missing.txt") |
IndexError |
List index out of range: [1,2][5] |
KeyError |
Missing dictionary key: {}["key"] |
AttributeError |
Object has no such attribute: "str".not_a_method() |
NameError |
Variable not defined: print(x) before x = 1 |
2.4 The else and finally Clauses
else runs only if no exception occurred:
try:
num = int(input("Number: "))
except ValueError:
print("Not a number!")
else:
print(f"Success! You entered {num}.")
finally runs no matter what — exception or not. Perfect for cleanup (closing files, releasing resources):
file = None
try:
file = open("data.txt", "r")
content = file.read()
except FileNotFoundError:
print("File not found.")
finally:
if file:
file.close() # Always runs, even if error occurred
print("Cleanup done.")
💡 With
with open(...) as f:, you don't needfinallyfor files —withhandles cleanup automatically. Butfinallyis essential for other resources (database connections, network sockets, etc.).
Order of execution:
try → [except if error] → [else if no error] → finally (always)
2.5 Raising Your Own Exceptions — raise
When your code detects an invalid state, you can deliberately raise an exception:
def set_age(age):
if age < 0:
raise ValueError("Age cannot be negative.")
print(f"Age set to {age}.")
# set_age(-5) # ValueError: Age cannot be negative.
Re-raising exceptions — catch an exception, do something (like logging), then re-raise it:
try:
result = 10 / 0
except ZeroDivisionError:
print("Logging this error...")
raise # Re-raises the original exception
2.6 Understanding Tracebacks
When an unhandled exception occurs, Python prints a traceback — a stack trace showing exactly where the error happened:
Traceback (most recent call last):
File "app.py", line 4, in <module>
result = 10 / number
ZeroDivisionError: division by zero
How to read a traceback:
Read from bottom to top. The last line tells you what exception occurred and its message.
Look at the file and line number right above it for the exact location.
If multiple function calls are involved, the traceback shows the entire call stack — start at the top for the root cause.
2.7 Basic Debugging Techniques
1. Print debugging (the most straightforward):
def calculate(x, y):
print(f"DEBUG: x={x}, y={y}") # See what's coming in
result = x / y
print(f"DEBUG: result={result}")
return result
2. assert statements — check conditions during development:
def calculate_average(grades):
assert len(grades) > 0, "grades list is empty"
return sum(grades) / len(grades)
When an assert fails, it raises AssertionError. Assertions can be disabled with the -O flag for production, so never use them for actual validation — use if/raise for that.
3. pdb — Python Debugger (quick overview):
import pdb
def buggy_function(x, y):
pdb.set_trace() # Program pauses here; interactive debugger starts
return x / y
In pdb, you can inspect variables (p x), step through lines (n), continue (c), etc. We'll explore more later, but know it exists.
4. logging module (preview for advanced use):
import logging
logging.basicConfig(level=logging.DEBUG)
logging.debug("This is debug info")
logging.info("Program started")
logging.warning("This is a warning")
logging.error("An error occurred")
2.8 Common Mistakes & Anti-Patterns
| Mistake | Why It's Harmful |
|---|---|
Bare except: |
Hides Ctrl+C and unexpected errors; makes debugging impossible |
| Catching too broadly | except Exception: may swallow critical errors you want to see |
Catching BaseException |
It includes SystemExit and KeyboardInterrupt, which you shouldn't suppress |
pass inside except |
Silently swallows errors — you'll never know something went wrong |
| Not logging the original exception | Always print or log the traceback |
3. Code Examples
Example 1: Robust Number Input
def get_positive_float(prompt):
"""Keep asking until the user enters a valid positive float."""
while True:
try:
value = float(input(prompt))
if value <= 0:
print("Please enter a positive number.")
continue
return value
except ValueError:
print("That's not a valid number. Try again.")
price = get_positive_float("Enter price: $")
print(f"Price: ${price:.2f}")
Example 2: Safe File Reader
def read_file_safely(filename):
"""Read a file, handling missing file gracefully."""
try:
with open(filename, "r") as f:
return f.read()
except FileNotFoundError:
print(f"Error: '{filename}' not found.")
return None
except PermissionError:
print(f"Error: Permission denied for '{filename}'.")
return None
content = read_file_safely("config.cfg")
if content:
print(content)
Example 3: Custom Exception Class
class InsufficientFundsError(Exception):
"""Raised when a withdrawal exceeds the balance."""
pass
def withdraw(balance, amount):
if amount > balance:
raise InsufficientFundsError(
f"Cannot withdraw ${amount:.2f}. Balance: ${balance:.2f}"
)
return balance - amount
try:
new_balance = withdraw(100, 150)
except InsufficientFundsError as e:
print(e)
4. Hands-On Exercises
Exercise 1: Safe Division
Write a function safe_divide(a, b) that returns a / b. Handle ZeroDivisionError and TypeError (if non-numbers are passed). Return None on error and print a helpful message.
Exercise 2: List Index Accessor
Ask the user for an index, then print the element at that index from a predefined list ["Python", "Java", "C++", "JavaScript"]. Handle IndexError and ValueError gracefully.
Exercise 3: File Existence Checker
Write a function that asks for a filename. Try to open and read it. Handle FileNotFoundError by asking the user if they want to create the file. If yes, create it and write "Created by Python".
Exercise 4: Password Validator (with exceptions)
Write a function validate_password(pw) that:
Raises
ValueErrorif password is shorter than 8 charactersRaises
ValueError
Comments
Post a Comment
Leave us your comments here...