🛡️ Error Handling: Your Code’s Safety Net
The Big Picture: Why Errors Happen
Imagine you’re a chef in a kitchen. Everything is going great—until someone asks for a dish you’ve never made. Or the oven breaks. Or you run out of ingredients.
What do you do? You don’t just freeze! You have a plan:
- If the oven breaks → use the stovetop
- If you’re missing an ingredient → find a substitute
- No matter what → clean up the kitchen at the end
That’s exactly what Exception Handling does for your code! It’s your safety net that catches problems before they crash your program.
🎯 What You’ll Learn
graph LR A["Exception Handling"] --> B["try-except Blocks"] A --> C["Multiple Exceptions"] A --> D["else Clause"] A --> E["finally Clause"] A --> F["Raising Exceptions"] A --> G["Exception Chaining"] A --> H["Custom Exceptions"] A --> I["Built-in Exceptions"]
đź§± 1. try-except Blocks: The Foundation
What’s the Story?
Think of try-except like a protective bubble. You put risky code inside the bubble. If something goes wrong, the bubble catches it instead of letting it break everything.
The Simple Pattern
try:
# Risky code goes here
result = 10 / 0
except:
# What to do if it fails
print("Oops! Something broke!")
Real Example: Dividing Numbers
try:
number = int(input("Enter a number: "))
result = 100 / number
print(f"100 divided by {number} = {result}")
except:
print("That didn't work!")
What happens:
- User enters
5→ prints20.0✅ - User enters
0→ prints “That didn’t work!” ✅ - User enters
abc→ prints “That didn’t work!” ✅
Catching Specific Errors
Be a detective! Catch the exact error:
try:
number = int(input("Enter a number: "))
result = 100 / number
except ZeroDivisionError:
print("You can't divide by zero!")
except ValueError:
print("That's not a number!")
Now you know exactly what went wrong!
🎯 2. Catching Multiple Exceptions
The Story
Sometimes, different things can go wrong. Like a doctor who can treat both a cold AND a broken arm—your code can handle multiple problems!
Method 1: Separate except Blocks
try:
file = open("data.txt")
number = int(file.read())
result = 100 / number
except FileNotFoundError:
print("File doesn't exist!")
except ValueError:
print("File doesn't have a number!")
except ZeroDivisionError:
print("Can't divide by zero!")
Method 2: Group Similar Errors
When you want the same response for different errors:
try:
value = int(input("Enter age: "))
result = 100 / value
except (ValueError, TypeError):
print("Need a valid number!")
except ZeroDivisionError:
print("Age can't be zero!")
Getting Error Details
Want to know exactly what went wrong?
try:
result = 10 / 0
except ZeroDivisionError as e:
print(f"Error details: {e}")
# Output: Error details: division by zero
✨ 3. The else Clause: Celebrate Success!
The Story
Imagine you’re baking a cake. The try block is mixing ingredients. The except block handles disasters. But what if everything goes perfectly? That’s when else runs—only when nothing went wrong!
Pattern
try:
# Risky stuff
except SomeError:
# Handle problems
else:
# Only runs if NO errors happened
Real Example
try:
age = int(input("Enter your age: "))
except ValueError:
print("That's not a number!")
else:
print(f"In 10 years, you'll be {age + 10}!")
Why Use else?
Without else:
try:
number = int(input("Number: "))
print(f"Double: {number * 2}") # This runs even if next line fails
except ValueError:
print("Invalid!")
With else (cleaner!):
try:
number = int(input("Number: "))
except ValueError:
print("Invalid!")
else:
print(f"Double: {number * 2}") # Only runs if try succeeded
đź”’ 4. The finally Clause: Always Do This!
The Story
Imagine you’re borrowing a friend’s toy. No matter what happens—whether you play with it or accidentally break it—you must return it. That’s finally!
Pattern
try:
# Try something risky
except SomeError:
# Handle the error
finally:
# THIS ALWAYS RUNS no matter what!
Classic Example: Closing Files
file = None
try:
file = open("data.txt", "r")
content = file.read()
print(content)
except FileNotFoundError:
print("File not found!")
finally:
if file:
file.close()
print("File closed!")
finally Runs Even When…
âś… The code works perfectly
âś… An error happens
âś… You use return inside try
âś… You use break in a loop
def get_number():
try:
return int("abc") # This fails
except ValueError:
return 0 # Returns 0
finally:
print("Cleanup!") # Still prints!
result = get_number()
# Output: Cleanup!
# result = 0
🚀 5. Raising Exceptions: Sound the Alarm!
The Story
Sometimes YOU need to signal a problem. Like a lifeguard blowing a whistle when they see danger. You raise an exception to say “STOP! Something’s wrong!”
Basic Pattern
raise ExceptionType("Your message here")
Example: Validating Input
def set_age(age):
if age < 0:
raise ValueError("Age can't be negative!")
if age > 150:
raise ValueError("That's too old!")
return age
# Usage:
try:
my_age = set_age(-5)
except ValueError as e:
print(f"Problem: {e}")
# Output: Problem: Age can't be negative!
Re-raising Exceptions
Sometimes you want to handle an error AND pass it along:
def process_data(data):
try:
result = int(data)
except ValueError:
print("Logging: Bad data received")
raise # Re-raise the same error!
try:
process_data("abc")
except ValueError:
print("Caller handles it too!")
đź”— 6. Exception Chaining: Tell the Full Story
The Story
Imagine a detective solving a case. The crime (error) you see might be caused by an earlier crime. Exception chaining connects them so you see the whole story.
Pattern: from Keyword
try:
value = int("abc")
except ValueError as original:
raise TypeError("Can't process data") from original
Real Example
def load_config(filename):
try:
with open(filename) as f:
return f.read()
except FileNotFoundError as e:
raise RuntimeError("Config failed!") from e
try:
config = load_config("missing.txt")
except RuntimeError as e:
print(f"Error: {e}")
print(f"Caused by: {e.__cause__}")
Output:
Error: Config failed!
Caused by: [Errno 2] No such file or directory: 'missing.txt'
Hiding the Chain
Sometimes you want to hide the original error:
raise NewError("Message") from None
🎨 7. Custom Exceptions: Your Own Error Types
The Story
Built-in exceptions are like generic medicine. But sometimes you need a specific solution. Custom exceptions let you create error types that make sense for YOUR program!
Creating a Custom Exception
class AgeError(Exception):
"""Raised when age is invalid"""
pass
def check_age(age):
if age < 0:
raise AgeError("Age cannot be negative!")
if age < 18:
raise AgeError("Must be 18 or older!")
return True
Adding More Features
class InsufficientFundsError(Exception):
def __init__(self, balance, amount):
self.balance = balance
self.amount = amount
self.needed = amount - balance
message = f"Need ${self.needed} more!"
super().__init__(message)
def withdraw(balance, amount):
if amount > balance:
raise InsufficientFundsError(balance, amount)
return balance - amount
try:
withdraw(50, 100)
except InsufficientFundsError as e:
print(f"Error: {e}")
print(f"You have: ${e.balance}")
print(f"You need: ${e.needed} more")
Best Practice: Exception Hierarchy
class GameError(Exception):
"""Base for all game errors"""
pass
class PlayerError(GameError):
"""Player-related errors"""
pass
class InventoryError(GameError):
"""Inventory-related errors"""
pass
# Now you can catch all game errors:
try:
# game code
pass
except GameError:
# Catches PlayerError AND InventoryError
pass
📚 8. Common Built-in Exceptions
The Most Common Ones
| Exception | When It Happens | Example |
|---|---|---|
ValueError |
Wrong value type | int("abc") |
TypeError |
Wrong data type | "2" + 2 |
IndexError |
List index out of range | [1,2,3][10] |
KeyError |
Dictionary key missing | {"a":1}["b"] |
FileNotFoundError |
File doesn’t exist | open("missing.txt") |
ZeroDivisionError |
Dividing by zero | 10 / 0 |
AttributeError |
Object lacks attribute | "hi".push() |
NameError |
Variable not defined | print(xyz) |
Exception Hierarchy
graph TD A["BaseException"] --> B["Exception"] B --> C["ValueError"] B --> D["TypeError"] B --> E["KeyError"] B --> F["IndexError"] B --> G["FileNotFoundError"] B --> H["ZeroDivisionError"] A --> I["SystemExit"] A --> J["KeyboardInterrupt"]
Quick Examples
# ValueError
try:
int("hello")
except ValueError:
print("Can't convert to number!")
# KeyError
try:
data = {"name": "Alex"}
print(data["age"])
except KeyError:
print("Key doesn't exist!")
# IndexError
try:
colors = ["red", "blue"]
print(colors[5])
except IndexError:
print("Index out of range!")
# AttributeError
try:
number = 42
number.append(1)
except AttributeError:
print("Numbers don't have append!")
🏆 Putting It All Together
Here’s a complete example using everything you learned:
class WithdrawalError(Exception):
"""Custom error for bank withdrawals"""
pass
def withdraw_money(balance, amount):
try:
# Validate input
if not isinstance(amount, (int, float)):
raise TypeError("Amount must be a number!")
if amount <= 0:
raise ValueError("Amount must be positive!")
if amount > balance:
raise WithdrawalError(
f"Not enough funds! Balance: ${balance}"
)
# Process withdrawal
new_balance = balance - amount
except TypeError as e:
print(f"Type error: {e}")
raise
except ValueError as e:
print(f"Value error: {e}")
raise
else:
print(f"Success! New balance: ${new_balance}")
return new_balance
finally:
print("Transaction logged.")
# Test it:
try:
result = withdraw_money(100, 50)
except Exception as e:
print(f"Transaction failed: {e}")
🎯 Key Takeaways
- try-except = Your safety net for risky code
- Multiple except = Handle different errors differently
- else = Runs only when try succeeds
- finally = Always runs, no matter what
- raise = You create the error when needed
- Chaining = Connect related errors
- Custom exceptions = Your own error types
- Built-ins = Know the common ones!
💪 You’ve Got This!
Exception handling isn’t scary—it’s your superpower! With these tools, your programs will:
- Never crash unexpectedly âś…
- Give helpful error messages âś…
- Clean up properly âś…
- Handle anything users throw at them âś…
Now go build something amazing—and don’t be afraid of errors. You know how to handle them! 🚀
