- try-except — the basic way to handle exceptions
- Multiple except — handling different types of exceptions
- else block — code that runs when no exception occurs
- finally block — code that always runs
- raise — manually raising exceptions
- Custom exceptions — creating your own exception types
- Best practices — writing clean exception handling code
What is Exception Handling?
Exception handling is the process of responding to runtime errors in a controlled way. Instead of letting your program crash, you can catch exceptions and handle them gracefully.
Python provides several keywords for exception handling:
- try — the block of code to test for errors
- except — the block of code to run if an error occurs
- else — the block of code to run if no error occurs
- finally — the block of code that always runs
- raise — manually trigger an exception
💡 Key concept: Exception handling makes your code more robust and user-friendly. Instead of crashing, your program can explain what went wrong and continue running.
try-except Block
The Basic Way to Handle Exceptions
The try-except block is the foundation of exception handling. You put the code that might cause an error in the try block, and the code to handle the error in the except block.
# Basic try-except
try:
result = 10 / 0
print(f"Result: {result}")
except ZeroDivisionError:
print("Cannot divide by zero!")
# Output:
# Cannot divide by zero!
# Handling a ValueError
try:
number = int("hello")
print(f"Number: {number}")
except ValueError:
print("That's not a valid number!")
# Handling multiple types
try:
num = int(input("Enter a number: "))
result = 10 / num
print(f"Result: {result}")
except ValueError:
print("That's not a valid number!")
except ZeroDivisionError:
print("Cannot divide by zero!")
Key points:
- Put risky code in the try block
- Catch specific exceptions in except blocks
- If an exception occurs, the rest of the try block is skipped
- The except block runs when the exception is caught
Quick Check: What happens when an exception occurs in the try block? (Answer: The rest of the try block is skipped, and the except block runs)
Multiple Except Blocks
Handling Different Types of Exceptions
You can have multiple except blocks to handle different types of exceptions. Each block catches a specific exception type.
# Multiple except blocks
def divide_numbers(a, b):
try:
result = a / b
return result
except ZeroDivisionError:
return "Cannot divide by zero!"
except TypeError:
return "Both arguments must be numbers!"
# Test the function
print(divide_numbers(10, 2)) # 5.0
print(divide_numbers(10, 0)) # Cannot divide by zero!
print(divide_numbers(10, "5")) # Both arguments must be numbers!
# Catching exceptions with the exception object
try:
result = 10 / 0
except ZeroDivisionError as e:
print(f"Error: {e}") # Error: division by zero
# Combining multiple exceptions in one except
try:
num = int("hello")
result = 10 / num
except (ValueError, ZeroDivisionError) as e:
print(f"Error occurred: {e}")
Key points:
- Each except block handles a specific exception type
- You can access the exception object using as
- You can combine multiple exceptions in one except block
- The order matters — catch more specific exceptions first
Quick Check: Can you combine multiple exceptions in one except block? (Answer: Yes, using a tuple: except (TypeError, ValueError))
Catching All Exceptions
Handling Any Exception
You can catch all exceptions using a bare except or catching Exception. However, this should be used carefully as it can hide important errors.
# Catching all exceptions
try:
result = 10 / 0
except Exception as e:
print(f"An error occurred: {e}")
# Using a bare except (not recommended)
try:
result = 10 / 0
except: # Catches everything
print("Something went wrong")
# Catching specific exceptions first
try:
result = 10 / 0
except ZeroDivisionError:
print("Cannot divide by zero!")
except Exception as e:
print(f"Some other error: {e}")
# When to use catch-all:
# 1. Logging errors
# 2. Cleaning up resources
# 3. Preventing crashes in critical applications
Guidelines:
- Avoid bare except — it catches everything
- Use except Exception for a catch-all
- Always catch specific exceptions first
- Use catch-all only when necessary
Quick Check: What is the problem with using a bare except? (Answer: It catches everything, including system exits and keyboard interrupts)
The else Block
Code That Runs When No Exception Occurs
The else block runs only if the try block completes without raising any exceptions. It's useful for code that should only run when everything goes well.
# Using the else block
try:
number = int(input("Enter a number: "))
except ValueError:
print("That's not a valid number!")
else:
print(f"You entered: {number}")
print("No errors occurred!")
# Why use else?
# 1. It separates error handling from success code
# 2. It prevents accidentally catching exceptions in the success code
# 3. It makes the code more readable
# Practical example
def read_file(filename):
try:
file = open(filename, "r")
except FileNotFoundError:
print(f"File '{filename}' not found!")
return None
else:
content = file.read()
file.close()
return content
finally:
# Code that always runs (covered next)
pass
Key points:
- The else block runs only when no exception occurs
- It's optional — you don't have to use it
- It makes the success path clear
- It prevents accidental catches in the success code
Quick Check: When does the else block run? (Answer: Only when no exception occurs in the try block)
The finally Block
Code That Always Runs
The finally block always runs, regardless of whether an exception occurred or not. It's perfect for cleanup code like closing files or releasing resources.
# Using the finally block
try:
file = open("data.txt", "r")
content = file.read()
print(content)
except FileNotFoundError:
print("File not found!")
finally:
print("This always runs!")
# Clean up resources
try:
file.close()
except:
pass
# Finally without except
try:
print("Trying something...")
result = 10 / 2
finally:
print("This always runs!")
# Output:
# Trying something...
# This always runs!
# Finally with an exception
try:
print("Trying something...")
result = 10 / 0
finally:
print("This always runs!")
# This runs, then the exception is raised
Key points:
- The finally block always runs
- Even if an exception occurs, finally still runs
- Even if there's a return statement, finally runs
- Used for cleanup (closing files, connections)
Quick Check: Does the finally block always run? (Answer: Yes, always — even if an exception occurs or there's a return)
Raising Exceptions
Manually Triggering Exceptions
You can use the raise keyword to manually trigger an exception. This is useful when you detect an error condition and want to stop execution or pass the error to the caller.
# Raising a built-in exception
def divide(a, b):
if b == 0:
raise ValueError("Cannot divide by zero!")
return a / b
# Using the function
try:
result = divide(10, 0)
except ValueError as e:
print(f"Error: {e}")
# Raising with a custom message
def check_age(age):
if age < 0:
raise ValueError("Age cannot be negative!")
if age < 18:
raise ValueError("Age must be 18 or older!")
return "Access granted"
# Re-raising an exception
try:
result = 10 / 0
except ZeroDivisionError:
print("Caught an error, re-raising...")
raise # Re-raises the same exception
# Raising a specific exception type
def get_value(index, data):
if index < 0 or index >= len(data):
raise IndexError("Index out of range!")
return data[index]
Key points:
- Use raise to manually trigger an exception
- You can raise built-in or custom exceptions
- You can add a custom message
- Use raise without arguments to re-raise
Quick Check: What keyword is used to manually trigger an exception? (Answer: raise)
Custom Exceptions
Creating Your Own Exception Types
You can create custom exceptions by inheriting from the Exception class. This is useful for creating meaningful, application-specific errors.
# Creating a custom exception
class InvalidAgeError(Exception):
"""Raised when an age is invalid"""
pass
class InsufficientFundsError(Exception):
"""Raised when an account doesn't have enough funds"""
def __init__(self, balance, amount):
self.balance = balance
self.amount = amount
super().__init__(f"Insufficient funds: balance {balance}, needed {amount}")
# Using custom exceptions
def validate_age(age):
if age < 0:
raise InvalidAgeError("Age cannot be negative!")
if age > 150:
raise InvalidAgeError("Age cannot be greater than 150!")
return True
def withdraw(balance, amount):
if amount > balance:
raise InsufficientFundsError(balance, amount)
return balance - amount
# Handling custom exceptions
try:
validate_age(-5)
except InvalidAgeError as e:
print(f"Age validation failed: {e}")
try:
balance = 100
new_balance = withdraw(balance, 150)
except InsufficientFundsError as e:
print(f"Withdrawal failed: {e}")
print(f"Balance: {e.balance}, Attempted: {e.amount}")
Key points:
- Custom exceptions inherit from Exception
- You can add custom attributes to carry data
- You can add custom messages
- Custom exceptions make your code more readable
Quick Check: What class should custom exceptions inherit from? (Answer: Exception)
Best Practices
Writing Clean Exception Handling Code
# 1. Be specific — catch only what you can handle
# GOOD
try:
result = 10 / 0
except ZeroDivisionError:
print("Cannot divide by zero!")
# AVOID
try:
result = 10 / 0
except: # Bare except
print("Something went wrong")
# 2. Use finally for cleanup
# GOOD
try:
file = open("data.txt", "r")
content = file.read()
finally:
file.close() # Always closes the file
# 3. Don't swallow exceptions silently
# AVOID
try:
risky_operation()
except:
pass # Silently ignores the error
# GOOD
try:
risky_operation()
except Exception as e:
print(f"Error occurred: {e}") # At least logs it
# 4. Use custom exceptions for clarity
# GOOD
class ConfigurationError(Exception):
pass
def load_config():
raise ConfigurationError("Config file not found")
# 5. Keep try blocks small
# GOOD
try:
result = divide(10, 2)
except ZeroDivisionError:
print("Cannot divide by zero!")
# Try blocks should only contain code that might raise the exception
Summary of best practices:
- Be specific — catch only what you can handle
- Use finally — for cleanup code
- Don't swallow — at least log errors
- Use custom exceptions — for clarity
- Keep try blocks small — target specific code
Common Mistakes
Things to Watch Out For
Using a Bare Except
# WRONG — catches everything, including keyboard interrupts
try:
while True:
process_data()
except: # Bare except
print("Stopped")
# CORRECT — catch specific exceptions
try:
while True:
process_data()
except KeyboardInterrupt:
print("User stopped the program")
except Exception as e:
print(f"Error: {e}")
Not Using else for Success Code
# AVOID — mixing success code with error handling
try:
file = open("data.txt", "r")
content = file.read()
file.close()
print(content) # This could raise an error too
except FileNotFoundError:
print("File not found")
# BETTER — use else for success code
try:
file = open("data.txt", "r")
except FileNotFoundError:
print("File not found")
else:
content = file.read()
file.close()
print(content) # This won't be caught accidentally
Swallowing Exceptions Silently
# WRONG — hides the error completely
try:
process_data()
except:
pass
# CORRECT — at least log or report it
try:
process_data()
except Exception as e:
print(f"Error processing data: {e}")
# Or log it: logger.error(f"Error processing data: {e}")
Quick Check: What is the most common mistake in exception handling? (Answer: Using a bare except or swallowing exceptions silently)
Try It Yourself
Experiment with exception handling in the editor below. Modify the code and see what happens.
EXCEPTION HANDLING PRACTICE
========================================
1. BASIC TRY-EXCEPT
Caught ZeroDivisionError!
2. MULTIPLE EXCEPT BLOCKS
divide(10, 2): 5.0
divide(10, 0): Cannot divide by zero!
divide(10, '5'): Both arguments must be numbers!
3. ELSE BLOCK
No errors! Result: 2.0
4. FINALLY BLOCK
Trying something...
This always runs!
5. RAISING EXCEPTIONS
Validation failed: Age cannot be negative!
Exception handling practice complete!
You've Got It!
You now understand exception handling in Python — try-except, multiple except blocks, else, finally, raising exceptions, and custom exceptions. This is a essential skill for writing robust Python code!
Quick Quiz
Test what you've learned:
Frequently Asked Questions
What is the difference between except and finally?
except runs only when an exception occurs. finally runs always, regardless of whether an exception occurred or not. Use except for error handling and finally for cleanup.
Should I use a bare except?
except catches everything, including system exits and keyboard interrupts. Use except Exception or catch specific exceptions instead.
Can I have multiple except blocks?
except blocks to handle different types of exceptions. The order matters — catch more specific exceptions first.
What is the purpose of the else block?
else block runs only when no exception occurs in the try block. It's useful for code that should only run on the success path.
How do I create a custom exception?
Exception. You can add custom attributes and methods. Example: class MyError(Exception): pass
What's a common interview question about exception handling?
Where to Go From Here
Now that you understand exception handling, check out these related topics:
User Defined Exception
Learn how to create your own custom exceptions.
Learn More →Logging Exception
Learn how to log exceptions for debugging and monitoring.
Learn More →Exception Assignments
Practice what you've learned with assignments.
Learn More →