18 Errors and Exceptions - Handling the Unexpected
18.1 Chapter Outline
- Understanding error types in Python
- Python’s exception handling mechanism
- Using try/except blocks
- Handling specific exceptions
- Creating more robust code
- Best practices for error handling
- Using exceptions in real applications
18.2 Learning Objectives
By the end of this chapter, you will be able to: - Identify the main types of errors in Python programs - Understand what exceptions are and how they work - Write try/except blocks to handle runtime errors - Handle specific exception types appropriately - Make your programs more resilient to errors - Create user-friendly error messages - Apply exception handling in practical applications
18.3 1. Introduction: When Things Go Wrong
Even the most experienced programmers write code with errors. The difference between novice and expert programmers isn’t whether they make mistakes—it’s how they anticipate and handle those mistakes. In Python (and most programming languages), errors generally fall into three categories:
- Syntax Errors: Mistakes in the structure of your code that prevent it from running
- Runtime Errors: Errors that occur while your program is running
- Logical Errors: Your code runs but doesn’t do what you expect
This chapter focuses primarily on runtime errors and how Python’s exception handling system allows you to deal with them gracefully.
AI Tip: Ask your AI assistant to analyze error messages you encounter and explain them in simple terms, highlighting exactly what went wrong and why.
18.4 2. Understanding Error Types
18.4.1 Syntax Errors
Syntax errors occur when you break Python’s grammar rules. The Python interpreter catches these when it tries to parse your code, preventing your program from running at all.
# Syntax error: missing closing parenthesis
print("Hello, world!"
The Python interpreter would respond with something like:
File "<stdin>", line 1
print("Hello, world!"
^
SyntaxError: unexpected EOF while parsing
Syntax errors are usually easy to fix once you understand what’s wrong.
18.4.2 Runtime Errors (Exceptions)
Runtime errors, also called exceptions, occur during program execution. Unlike syntax errors, the code is valid Python, but something goes wrong when it runs. For example:
# This code is syntactically correct but will cause a runtime error
= 10
x = 0
y = x / y # ZeroDivisionError result
When you run this, Python raises an exception:
Traceback (most recent call last):
File "<stdin>", line 1, in <module>
ZeroDivisionError: division by zero
Common runtime errors include: - ZeroDivisionError
: Trying to divide by zero - TypeError
: Performing an operation on incompatible types - ValueError
: Giving a function the right type but invalid value - IndexError
: Trying to access a non-existent index in a sequence - KeyError
: Trying to access a non-existent key in a dictionary - FileNotFoundError
: Trying to open a file that doesn’t exist - NameError
: Using a variable that hasn’t been defined
18.4.3 Logical Errors
Logical errors are the trickiest to find. Your code runs without raising exceptions, but it doesn’t do what you expect. For example:
# Logical error: calculating average incorrectly
= [85, 90, 78]
scores = sum(scores) / 4 # Should be divided by 3 (the length of the list)
average print(average) # Returns 63.25 instead of 84.33
This chapter focuses on runtime errors (exceptions). For help with logical errors, see Chapter 17 on Debugging.
18.5 3. Python’s Exception Handling: try and except
Python provides a powerful mechanism for handling exceptions: the try
/except
block. Here’s the basic structure:
try:
# Code that might cause an exception
= 10 / 0
result except:
# Code that runs if an exception occurs
print("Something went wrong!")
The code inside the try
block is executed. If an exception occurs, Python immediately jumps to the except
block, skipping any remaining code in the try
block.
18.5.1 A Simple Example
Let’s explore a simple example to see how exception handling works:
# Without exception handling
= 10
x = 0
y # result = x / y # Program crashes with ZeroDivisionError
# With exception handling
try:
= x / y
result print(f"The result is {result}")
except:
print("Cannot divide by zero!")
= None
result
print("Program continues executing...")
In the first case, the program would crash. In the second case, it captures the error, provides a useful message, and continues running.
18.6 4. Handling Specific Exceptions
The previous example catches any exception, but it’s usually better to catch specific exception types. This allows different handling for different errors:
try:
= int(input("Enter a number: "))
number = 100 / number
result print(f"100 divided by {number} is {result}")
except ValueError:
print("That's not a valid number!")
except ZeroDivisionError:
print("You cannot divide by zero!")
You can even catch multiple specific exceptions with a single handler:
try:
# Code that might raise different exceptions
# ...
except (ValueError, TypeError):
print("There was a problem with the data type or value")
18.7 5. Capturing Exception Information
Sometimes you want to display or log the actual error message. You can capture the exception object using the as
keyword:
try:
with open("nonexistent_file.txt", "r") as file:
= file.read()
content except FileNotFoundError as err:
print(f"Error: {err}")
# Could display: Error: [Errno 2] No such file or directory: 'nonexistent_file.txt'
This is especially useful for debugging or providing detailed feedback.
18.8 6. The else and finally Clauses
Python’s exception handling has two additional clauses:
18.8.1 The else Clause
The else
clause runs if the try
block completes without an exception:
try:
= int(input("Enter a number: "))
number = 100 / number
result except ValueError:
print("That's not a valid number!")
except ZeroDivisionError:
print("You cannot divide by zero!")
else:
# This runs only if no exceptions occurred
print(f"The result is {result}")
18.8.2 The finally Clause
The finally
clause runs whether an exception occurred or not. It’s useful for cleanup operations:
try:
file = open("data.txt", "r")
= file.read()
content except FileNotFoundError:
print("The file does not exist")
finally:
# This runs regardless of what happened in the try block
if 'file' in locals() and not file.closed:
file.close()
print("File closed successfully")
The finally
block is excellent for ensuring resources like files or network connections are properly closed.
18.9 7. Preventing Errors vs. Handling Exceptions
There are often two approaches to dealing with potential errors:
18.9.1 LBYL (Look Before You Leap)
Check for potential problems before performing an operation:
# LBYL approach
if divisor != 0:
= dividend / divisor
result else:
= "Cannot divide by zero" result
18.9.2 EAFP (Easier to Ask Forgiveness than Permission)
Try the operation and handle any exceptions that occur:
# EAFP approach
try:
= dividend / divisor
result except ZeroDivisionError:
= "Cannot divide by zero" result
Python generally favors the EAFP approach (using try/except) as it’s usually cleaner and handles rare edge cases better. However, if checking is simple and the exception would be common, LBYL might be more appropriate.
18.10 8. Common Error Handling Patterns
Here are some patterns you’ll use frequently:
18.10.1 Input Validation
def get_integer_input(prompt):
"""Keep asking until a valid integer is provided."""
while True:
try:
return int(input(prompt))
except ValueError:
print("Please enter a valid integer.")
18.10.2 Safe File Operations
def read_file_safely(filename):
"""Attempt to read a file and handle potential errors."""
try:
with open(filename, 'r') as file:
return file.read()
except FileNotFoundError:
print(f"The file '{filename}' was not found.")
return None
except PermissionError:
print(f"You don't have permission to read '{filename}'.")
return None
except Exception as e:
print(f"An unexpected error occurred: {e}")
return None
18.10.3 Graceful Degradation
def get_user_profile(user_id):
"""Retrieve user data, falling back to defaults on errors."""
try:
# Primary data source
return database.get_user(user_id)
except DatabaseError:
try:
# Backup data source
return api.fetch_user(user_id)
except APIError:
# Last resort - return default profile
return {"name": "Guest", "access_level": "minimal"}
18.11 9. Self-Assessment Quiz
What is the main difference between a syntax error and an exception?
- Syntax errors occur during runtime; exceptions occur during compilation
- Syntax errors occur during parsing; exceptions occur during runtime
- Syntax errors are always fatal; exceptions can be handled
- There is no difference; they are different terms for the same thing
Which of the following is NOT a common exception type in Python?
ValueError
TypeError
SyntaxError
MemoryError
What does the following code print if the user enters “abc”?
try: = int(input("Enter a number: ")) num print(f"You entered {num}") except ValueError: print("Not a valid number") else: print("Valid input received")
- “You entered abc” followed by “Valid input received”
- “Not a valid number” followed by “Valid input received”
- “Not a valid number”
- It raises an unhandled exception
In what order are the blocks executed in a try-except-else-finally statement when no exception occurs?
- try → except → else → finally
- try → else → except → finally
- try → else → finally
- try → finally → else
What happens if an exception is raised in the
except
block of a try-except statement?- The program crashes with an unhandled exception
- The exception is automatically handled
- The program continues executing as if nothing happened
- The
finally
block handles the new exception
Answers & Feedback: 1. b) Syntax errors occur during parsing; exceptions occur during runtime — Syntax errors prevent your code from running at all 2. c) SyntaxError
— While this is an error in Python, it’s not considered an exception that you can catch with try/except 3. c) “Not a valid number” — The else block only runs if no exception occurs 4. c) try → else → finally — When no exception occurs, the except block is skipped 5. a) The program crashes with an unhandled exception — Exception handlers don’t protect against errors within themselves
18.12 10. Common Exception Handling Mistakes
- Catching too broadly: Using
except:
without specifying the exception type can catch unexpected errors - Silencing errors: Catching exceptions but not handling them properly can hide bugs
- Overusing try/except: Using exception handling when simple conditionals would be clearer
- Forgetting cleanup: Not using
finally
orwith
statements for resource management - Raising generic exceptions: Raising
Exception
instead of more specific types
18.13 Project Corner: Making Your Chatbot Robust with Error Handling
Let’s enhance our chatbot to handle errors gracefully, focusing on file operations:
import os
import datetime
import random
# Response patterns and templates from Chapter 14
= {
response_patterns "greetings": ["hello", "hi", "hey", "howdy"],
"farewells": ["bye", "goodbye", "see you", "cya"],
# other patterns...
}
= {
response_templates "greetings": ["Hello there!", "Hi! Nice to chat with you!"],
"farewells": ["Goodbye! Come back soon!", "See you later!"],
# other templates...
}
def get_response(user_input):
"""Get a response based on the user input."""
= user_input.lower()
user_input
for category, patterns in response_patterns.items():
for pattern in patterns:
if pattern in user_input:
return random.choice(response_templates[category])
return "I'm still learning. Can you tell me more?"
def save_conversation():
"""Save the current conversation to a file with error handling."""
try:
# Create the chats directory if it doesn't exist
if not os.path.exists('chats'):
'chats')
os.makedirs(
= datetime.datetime.now().strftime("%Y%m%d_%H%M%S")
timestamp = f"chats/chat_with_{user_name}_{timestamp}.txt"
filename
with open(filename, "w") as f:
f"Conversation with {bot_name} and {user_name}\n")
f.write(f"Date: {datetime.datetime.now().strftime('%Y-%m-%d %H:%M:%S')}\n\n")
f.write(
for entry in conversation_history:
f"{entry}\n")
f.write(
return f"Conversation saved to {filename}"
except PermissionError:
return "Sorry, I don't have permission to save in that location."
except OSError as e:
return f"Error saving conversation: {str(e)}"
except Exception as e:
return f"An unexpected error occurred: {str(e)}"
def load_conversation(filename):
"""Load a previous conversation from a file with error handling."""
try:
# Make sure the file is in the chats directory for security
if not filename.startswith('chats/'):
= f"chats/{filename}"
filename
with open(filename, "r") as f:
= f.readlines()
lines
print("\n----- Loaded Conversation -----")
for line in lines:
print(line.strip())
print("-------------------------------\n")
return True
except FileNotFoundError:
print(f"{bot_name}> Sorry, I couldn't find the file '{filename}'.")
show_available_chats()return False
except PermissionError:
print(f"{bot_name}> I don't have permission to read that file.")
return False
except UnicodeDecodeError:
print(f"{bot_name}> That doesn't appear to be a text file I can read.")
return False
except Exception as e:
print(f"{bot_name}> An error occurred: {str(e)}")
return False
def show_available_chats():
"""Show a list of available saved conversations with error handling."""
try:
if not os.path.exists('chats'):
print("No saved conversations found.")
return
= os.listdir('chats')
chat_files if not chat_files:
print("No saved conversations found.")
return
print("\nAvailable saved conversations:")
for i, chat_file in enumerate(chat_files, 1):
print(f"{i}. {chat_file}")
except Exception as e:
print(f"Error listing conversations: {str(e)}")
def get_valid_input(prompt, validation_func=None, error_message=None):
"""Repeatedly prompt the user until valid input is received."""
while True:
= input(prompt)
user_input
# If no validation function was provided, any input is valid
if validation_func is None:
return user_input
# Check if the input is valid
if validation_func(user_input):
return user_input
# Display error message and try again
if error_message:
print(error_message)
# Main chat loop
= "PyBot"
bot_name print(f"Hello! I'm {bot_name}. Type 'bye' to exit.")
print("Special commands:")
print("- 'save': Save the current conversation")
print("- 'chats': Show available saved conversations")
print("- 'load <filename>': Load a conversation")
# Get user name with validation
def is_valid_name(name):
return len(name.strip()) > 0
= get_valid_input(
user_name "What's your name? ",
is_valid_name,"Name cannot be empty. Please enter your name."
)print(f"Nice to meet you, {user_name}!")
= []
conversation_history
def save_to_history(speaker, text):
"""Save an utterance to conversation history."""
f"{speaker}: {text}")
conversation_history.append(
# Save initial greeting
f"Nice to meet you, {user_name}!")
save_to_history(bot_name,
while True:
try:
= input(f"{user_name}> ")
user_input
save_to_history(user_name, user_input)
# Check for special commands
if user_input.lower() == "bye":
= f"Goodbye, {user_name}!"
response print(f"{bot_name}> {response}")
save_to_history(bot_name, response)break
elif user_input.lower() == "save":
= save_conversation()
result print(f"{bot_name}> {result}")
save_to_history(bot_name, result)continue
elif user_input.lower() == "chats":
show_available_chats()continue
elif user_input.lower().startswith("load "):
= user_input[5:].strip()
filename
load_conversation(filename)continue
# Get and display response
= get_response(user_input)
response print(f"{bot_name}> {response}")
save_to_history(bot_name, response)
except KeyboardInterrupt:
# Handle Ctrl+C gracefully
print(f"\n{bot_name}> Conversation interrupted. Goodbye!")
break
except Exception as e:
# Catch-all for unexpected errors to prevent program crashes
= f"I encountered an error: {str(e)}"
error_msg print(f"{bot_name}> {error_msg}")
save_to_history(bot_name, error_msg)
This enhanced chatbot includes:
- Error handling for file operations (saving/loading)
- A validation function for user input
- Graceful handling of keyboard interrupts (Ctrl+C)
- Security measures for file access
- A catch-all exception handler to prevent crashes
- Informative error messages
These improvements make the chatbot more robust and user-friendly. When problems occur, the program doesn’t crash - it provides helpful information and continues running.
Challenges: - Add a log file that records errors for later review - Implement a system to recover from the last successful state after an error - Create more specific exception types for different chatbot-related errors - Add a “debug mode” that provides more detailed error information - Create a validation system for all user commands
18.14 Cross-References
- Previous Chapter: Files
- Next Chapter: Debugging
- Related Topics: Files (Chapter 15), Functions (Chapter 9)
AI Tip: Ask your AI assistant to help you convert cryptic Python error messages into plain English explanations that include specific suggestions for fixing the problem.
18.15 Error Handling in the Real World
Effective error handling is a hallmark of professional-quality code. Here are some real-world approaches:
18.15.1 Logging Instead of Printing
In production applications, errors are typically logged rather than printed:
import logging
# Configure logging
logging.basicConfig(='app.log',
filename=logging.ERROR,
levelformat='%(asctime)s - %(name)s - %(levelname)s - %(message)s'
)
try:
# Risky code here
= 10 / 0
result except Exception as e:
# Log the error with traceback information
"An error occurred during calculation") logging.exception(
18.15.2 Custom Exception Classes
For complex applications, custom exceptions can make error handling more specific:
class InsufficientFundsError(Exception):
"""Raised when a bank account has insufficient funds for a withdrawal."""
def __init__(self, account, amount, balance):
self.account = account
self.amount = amount
self.balance = balance
self.deficit = amount - balance
super().__init__(f"Cannot withdraw ${amount} from account {account}. Balance: ${balance}, deficit: ${self.deficit}")
# Using the custom exception
def withdraw(account_id, amount):
= get_account_balance(account_id)
balance if balance < amount:
raise InsufficientFundsError(account_id, amount, balance)
# Process withdrawal if sufficient funds
18.15.3 Error Recovery Strategies
Robust systems need strategies for recovering from errors:
- Retry with backoff: When temporary failures occur (like network issues)
- Fallback to alternatives: When a primary method fails, try a backup
- Graceful degradation: Continue with limited functionality rather than failing completely
- Checkpointing: Save progress frequently so you can recover from the last good state
- Circuit breakers: Stop trying operations that consistently fail
By implementing these strategies, you can create Python programs that not only handle errors gracefully but also recover from them effectively—a key skill for developing reliable software.