Previous | Tutorial index | Next
try...excepttry statement properly to handle possible exceptions raised by potential errors in a block of program code.In Tutorial 2, we learned about the various built-in exceptions that Python can raise. But knowing what can go wrong is only half the battle; the other half is knowing how to respond when things do go wrong. This is where Python's exception handling mechanism—centered around the try...except block—comes into play.
Proper exception handling is what separates a program that crashes at the first sign of trouble from one that gracefully recovers, provides useful feedback, and continues operating. It is a hallmark of professional, robust software.
In this tutorial, we will explore every aspect of the try statement, including:
try block: Where you put code that might raise an exception.except block: How to catch and handle specific exceptions.except blocks: Handling different exceptions differently.else clause: Code that runs only when no exception occurs.finally clause: Code that runs no matter what (cleanup actions).Before diving into the content, familiarise yourself with these key terms:
| Term | Definition |
|---|---|
try block |
Encloses code that may raise an exception. |
except block |
Catches and handles specific exceptions; you can have multiple. |
except (Error1, Error2) |
Catches multiple exception types in one block using a tuple. |
except ... as e |
Accesses the exception object for details (error message, attributes). |
else clause |
Runs only if no exception occurs in the try block. |
finally clause |
Always runs, regardless of exceptions; ideal for cleanup. |
Bare except: |
Catches all exceptions, including system-exiting ones – not recommended. |
| Exception chaining | Using raise ... from to link a new exception to the original cause. |
| Context manager | The with statement for automatic resource cleanup (alternative to finally). |
Imagine you are a pilot flying a commercial airliner. The flight is your program.
try block: You are flying the plane through a storm. You know things might go wrong (turbulence, engine trouble, etc.).except block: When an alarm sounds (an exception), you have pre-planned procedures: if it's engine fire, you shut down that engine; if it's loss of cabin pressure, you deploy oxygen masks. Each specific problem has a specific response.else block: If the flight goes smoothly without any alarms, you might still perform routine tasks, like updating the flight log, once you're clear of the storm.finally block: No matter what happens—whether you land safely or have to make an emergency landing—you always do certain things at the end: turn off the engines, secure the aircraft, and file a report. These actions are non-negotiable and must happen regardless of the outcome.This analogy illustrates the structure of Python's exception handling: you anticipate problems, respond to each appropriately, perform normal tasks if all is well, and always clean up afterwards.
try Block: The Starting PointThe try block is used to enclose a section of code that you suspect might raise an exception. You are saying, "I'm going to try to execute this code, but I acknowledge that it might fail, and if it does, I have a plan."
try:
# Code that may raise an exception
statement1
statement2
...
try block must be followed by at least one except block or a finally block.try block execute normally, and the except blocks are skipped.try block, the rest of the try block is skipped, and control jumps to the appropriate except block.except Block: Catching and Handling ExceptionsThe except block catches an exception that has been raised in the try block and allows you to respond to it. Without an except handler, an unhandled exception would terminate your program.
try:
risky_code()
except ExceptionType:
# Handle the exception
print("Something went wrong!")
It is best practice to catch specific exceptions rather than using a bare except:. This prevents your code from accidentally swallowing critical system exceptions like KeyboardInterrupt or SystemExit.
try:
number = int(input("Enter a number: "))
result = 10 / number
except ValueError:
print("That's not a valid number!")
except ZeroDivisionError:
print("Cannot divide by zero!")
You can use the as keyword to assign the exception object to a variable, allowing you to access its details (e.g., error message, arguments).
try:
with open("missing.txt", "r") as file:
content = file.read()
except FileNotFoundError as e:
print(f"File not found. Error details: {e}")
print(f"Error number: {e.errno}")
print(f"Error message: {e.strerror}")
If you want to handle several exceptions in the same way, you can group them in a tuple.
try:
value = int(input("Enter a number: "))
result = 100 / value
print(result)
except (ValueError, ZeroDivisionError) as e:
print(f"Input error: {e}")
except: – Use with CautionA bare except: catches all exceptions, including SystemExit and KeyboardInterrupt, which are generally not meant to be caught. This can make it difficult to terminate your program or to debug.
try:
risky_code()
except: # Bad practice
print("Something went wrong")
Better to catch Exception (which excludes system-exiting exceptions) or be even more specific.
try:
risky_code()
except Exception as e:
print(f"An error occurred: {e}")
except Blocks MattersPython checks except blocks in the order they appear. The first matching block is executed. Therefore, you should place more specific exceptions before more general ones.
try:
# Some code
except ZeroDivisionError: # Specific first
print("Division by zero")
except ArithmeticError: # More general parent
print("Some arithmetic error")
except Exception: # Very general
print("Any other error")
If you reversed the order, the general except ArithmeticError would catch the ZeroDivisionError before the specific block could, which is probably not what you want.
else Clause: Code That Runs on SuccessThe else clause is executed only if no exception was raised in the try block. It allows you to separate the "successful" code from the error-handling code, making both clearer.
try:
risky_code()
except SomeError:
print("Error handled")
else:
# Runs only if no exception occurred
print("All went well!")
elsetry block succeeds (e.g., confirming a successful file write, saving results).try:
with open("data.txt", "r") as file:
data = file.read()
except FileNotFoundError:
print("File not found, using default data.")
data = "default"
else:
print("File read successfully.")
# Process data here, knowing it's valid
processed = data.upper()
print(f"Processed data: {processed}")
finally Clause: Code That Always RunsThe finally clause is executed no matter what—whether an exception occurred or not, whether it was caught or not. It is used for cleanup actions that must happen regardless of the outcome.
try:
risky_code()
except SomeError:
print("Error occurred")
finally:
# Always runs
print("Cleanup performed")
file = None
try:
file = open("output.txt", "w")
file.write("Important data")
except OSError as e:
print(f"Could not write to file: {e}")
finally:
if file is not None:
file.close() # Always close the file
print("File closed.")
finally block runs even if the try block contains a return, break, or continue statement. The return value is stored, finally executes, and then the function returns.finally block itself, it will replace any pending exception from the try or except block.try...except...else...finallyYou can combine all four parts in one statement. The order is fixed:
try:
# Code that may raise an exception
except ExceptionType1:
# Handle specific exception
except ExceptionType2:
# Handle another exception
else:
# Runs if no exception
finally:
# Always runs
def safe_divide():
try:
numerator = float(input("Enter numerator: "))
denominator = float(input("Enter denominator: "))
result = numerator / denominator
except ValueError:
print("Please enter valid numbers.")
except ZeroDivisionError:
print("Cannot divide by zero.")
else:
print(f"The result is: {result}")
finally:
print("Division operation attempted.")
safe_divide()
try...except BlocksYou can nest exception handlers for fine-grained control. A common pattern is to have an inner try for a specific operation and an outer try for broader recovery.
try:
# Outer try for high-level handling
file = open("config.txt", "r")
try:
data = file.read()
number = int(data)
except ValueError:
print("Invalid integer in config file.")
finally:
file.close()
except FileNotFoundError:
print("Config file missing.")
Sometimes you want to catch an exception, do some logging or cleanup, and then re-raise it to be handled at a higher level. Use raise without arguments inside an except block.
try:
risky_operation()
except ValueError as e:
print(f"Logging: {e}")
raise # Re-raise the same exception
You can use raise to throw an exception intentionally, often after catching a low-level exception and converting it to a higher-level one.
try:
with open("data.txt", "r") as f:
content = f.read()
except FileNotFoundError:
raise RuntimeError("Data file is missing, cannot proceed.") from None
sys.exc_info() for DebuggingThe sys.exc_info() function returns a tuple (type, value, traceback) for the current exception, useful for logging.
import sys
try:
1 / 0
except:
exc_type, exc_value, exc_traceback = sys.exc_info()
print(f"Type: {exc_type}")
print(f"Value: {exc_value}")
# You can also use the traceback module to get a stack trace.
with) as an Alternative to finallyFor resource management, the with statement (context manager) automatically handles cleanup (e.g., closing files). It is often preferred over manual try...finally for such tasks.
# Instead of:
file = open("file.txt", "r")
try:
data = file.read()
finally:
file.close()
# Prefer:
with open("file.txt", "r") as file:
data = file.read()
# File is automatically closed when the with block exits.
However, with only handles resource cleanup; for other cleanup actions (like logging, restoring state), you still need try...finally.
| Pitfall | Solution |
|---|---|
Catching too broad an exception (bare except:) |
Catch specific exceptions or at least Exception. |
Catching KeyboardInterrupt unintentionally |
Don't use bare except:. |
| Swallowing exceptions silently | Log the error or re-raise if you can't handle it. |
Having an empty except block |
At least print a message or log; never leave it empty. |
Incorrect order of except blocks |
Put more specific exceptions first. |
| Forgetting to clean up resources | Use finally or with statements. |
| Overusing exceptions for flow control | Exceptions are for exceptional situations; use conditionals when possible. |
Catching Exception but not raising a useful message |
Include context in the error message. |
Let's combine all the concepts in a practical example. This function reads a list of numbers from a file, computes their average, and writes the result to another file. It handles all possible errors gracefully.
import os
def process_numbers(input_file, output_file):
try:
# Open input file
with open(input_file, 'r') as infile:
lines = infile.readlines()
except FileNotFoundError:
print(f"Error: Input file '{input_file}' not found.")
return
except PermissionError:
print(f"Error: Permission denied to read '{input_file}'.")
return
except OSError as e:
print(f"Error: OS error reading '{input_file}': {e}")
return
numbers = []
for line_num, line in enumerate(lines, 1):
line = line.strip()
if not line: # skip empty lines
continue
try:
num = float(line)
numbers.append(num)
except ValueError:
print(f"Warning: Line {line_num} contains invalid number '{line}'. Skipping.")
if not numbers:
print("No valid numbers found in the input file.")
return
average = sum(numbers) / len(numbers)
try:
with open(output_file, 'w') as outfile:
outfile.write(f"Average of {len(numbers)} numbers: {average:.2f}\n")
except PermissionError:
print(f"Error: Permission denied to write to '{output_file}'.")
except OSError as e:
print(f"Error: OS error writing to '{output_file}': {e}")
else:
print(f"Successfully wrote average to '{output_file}'.")
finally:
print("Processing completed.")
# Example usage:
process_numbers("data.txt", "average.txt")
| Component | Purpose |
|---|---|
try |
Contains code that may raise exceptions. |
except |
Catches and handles specific exceptions; you can have multiple. |
except (Error1, Error2) |
Catches multiple exception types in one block. |
except ... as e |
Access the exception object for details. |
else |
Runs only if no exception occurs in the try block. |
finally |
Always runs, regardless of exceptions; ideal for cleanup. |
Best Practices:
except blocks from most specific to most general.else to separate success code from error handling.finally or with for resource cleanup.Test your understanding of the concepts covered in this tutorial. Answer each question, then click to reveal the correct answer.
Q1: What is the primary purpose of the try block?
Q2: Which of the following is NOT a valid way to catch a specific exception?
except ValueError:except (ValueError, TypeError):except Exception as e:except:Q3: True or False: The else clause runs only if an exception is raised in the try block.
Q4: What is the output of the following code?
try:
print("A")
x = 1 / 0
print("B")
except ZeroDivisionError:
print("C")
else:
print("D")
finally:
print("E")
Q5: Which of the following is the correct order of clauses in a try statement?
try, else, except, finallytry, except, else, finallytry, except, finally, elsetry, finally, except, elseQ6: What happens if an exception is raised inside a finally block?
try or except block.Q7: Which of the following is a better practice than using a bare except:?
except Exception:except BaseException:except (ValueError, TypeError):Q8: If you have a try block with two except blocks: one for IndexError and one for LookupError, which one should come first?
except IndexError first, because it is more specific.except LookupError first, because it is more general.Q9: The following code uses a return inside the try block. Will the finally block execute?
def func():
try:
return 42
finally:
print("Finally!")
finally always executes.return exits the function before finally.Q10: When should you use the else clause instead of putting code after the try...except block?
except.else is never needed; you can always put code after the try block.Complete the exercises below to reinforce your understanding. Sample solutions are provided after each exercise.
Instructions: Write a program that asks the user for two numbers and then prints the result of dividing the first by the second. Handle the following exceptions:
ValueError: If the user enters non-numeric input.ZeroDivisionError: If the second number is zero.Your Program Structure:
ValueError).ZeroDivisionError).def divide_numbers():
try:
num1 = float(input("Enter the first number: "))
num2 = float(input("Enter the second number: "))
result = num1 / num2
print(f"Result: {result}")
except ValueError:
print("Error: Please enter valid numbers.")
except ZeroDivisionError:
print("Error: Cannot divide by zero.")
except Exception as e:
print(f"An unexpected error occurred: {e}")
finally:
print("Thank you for using the calculator.")
# Run it
divide_numbers()
Instructions: Write a function parse_and_square(data) that:
data.ValueError and TypeError in a single except block, printing "Invalid input: {error}".except block and printed.Test your function with:
parse_and_square("5") → should return 25.parse_and_square("abc") → should print "Invalid input: invalid literal for int() with base 10: 'abc'".parse_and_square([1,2]) → should print "Invalid input: int() argument must be a string...".def parse_and_square(data):
try:
number = int(data)
square = number * number
return square
except (ValueError, TypeError) as e:
print(f"Invalid input: {e}")
except Exception as e:
print(f"Unexpected error: {e}")
# Test
print(parse_and_square("5")) # 25
print(parse_and_square("abc")) # Invalid input: ...
print(parse_and_square([1, 2])) # Invalid input: ...
else and finallyInstructions: Write a program that attempts to open a file named "config.txt", read its content, and split it into lines. If the file is not found, create a default configuration file with the content "default_config". If the file is found successfully, print the number of lines. Always print "File operation completed." regardless of the outcome.
Hints:
try...except FileNotFoundError...else...finally.except block, create the file using open(filename, "w").else block, count the lines and print.finally block prints the message.def check_config():
try:
with open("config.txt", "r") as file:
lines = file.readlines()
except FileNotFoundError:
print("Config file not found. Creating default config.")
with open("config.txt", "w") as file:
file.write("default_config")
lines = []
else:
print(f"Config file has {len(lines)} lines.")
finally:
print("File operation completed.")
check_config()
Instructions: Write a function read_number_from_file(filename) that:
None and print "File not found.".None and print "Invalid integer in file.".with or a finally block).def read_number_from_file(filename):
try:
with open(filename, "r") as file:
line = file.readline().strip()
except FileNotFoundError:
print("File not found.")
return None
except Exception:
# Re-raise any other exception
raise
try:
number = int(line)
return number
except ValueError:
print("Invalid integer in file.")
return None
Instructions: Write a function safe_divide(a, b) that:
a by b.ZeroDivisionError occurs, print "Cannot divide by zero." and re-raise the exception.safe_divide(10, 0) inside a try...except that catches the re-raised exception and prints "Caught in main.".def safe_divide(a, b):
try:
return a / b
except ZeroDivisionError:
print("Cannot divide by zero.")
raise
except Exception as e:
print(f"An error occurred: {e}")
raise
# Main program
try:
result = safe_divide(10, 0)
except ZeroDivisionError:
print("Caught in main.")
1. You are building a program that processes a CSV file containing user data. The file has columns: name, age, email. Write a function process_users(filename) that:
age is an integer (convert it).email contains '@'.FileNotFoundError: Print "File not found." and return an empty list.PermissionError: Print "Permission denied." and return an empty list.OSError: Print "OS error occurred." and return an empty list.Bonus: Use a finally block to print "Processing complete." regardless of success or failure.
def process_users(filename):
users = []
try:
with open(filename, 'r') as file:
lines = file.readlines()
except FileNotFoundError:
print("File not found.")
return []
except PermissionError:
print("Permission denied.")
return []
except OSError as e:
print(f"OS error occurred: {e}")
return []
except Exception as e:
print(f"Unexpected error: {e}")
raise
finally:
print("Processing complete.")
# Skip header
for i, line in enumerate(lines[1:], start=2):
parts = line.strip().split(',')
if len(parts) < 3:
print(f"Warning: Line {i} has insufficient columns. Skipping.")
continue
name, age_str, email = parts[0], parts[1], parts[2]
try:
age = int(age_str)
except ValueError:
print(f"Warning: Line {i} has invalid age '{age_str}'. Skipping.")
continue
if '@' not in email:
print(f"Warning: Line {i} has invalid email '{email}'. Skipping.")
continue
users.append({"name": name, "age": age, "email": email})
return users
# Test (assuming file exists)
# users = process_users("users.csv")
2. Design a simple bank account class BankAccount with the following:
account_number, balance.deposit(amount): Adds amount to balance. Raises ValueError if amount is negative.withdraw(amount): Subtracts amount from balance if sufficient. Raises ValueError if amount is negative. Raises RuntimeError if balance is insufficient.get_balance(): Returns the current balance.Write a driver program that:
BankAccount object.deposit, withdraw, balance, or quit).try...except to handle the possible exceptions and prints appropriate error messages.finally block to print "Thank you for using our banking system." when the user quits.class BankAccount:
def __init__(self, account_number, initial_balance=0):
self.account_number = account_number
self.balance = initial_balance
def deposit(self, amount):
if amount < 0:
raise ValueError("Deposit amount cannot be negative.")
self.balance += amount
def withdraw(self, amount):
if amount < 0:
raise ValueError("Withdrawal amount cannot be negative.")
if amount > self.balance:
raise RuntimeError("Insufficient balance.")
self.balance -= amount
def get_balance(self):
return self.balance
# Driver program
def main():
account = BankAccount("12345", 100)
while True:
cmd = input("Enter command (deposit, withdraw, balance, quit): ").strip().lower()
if cmd == "quit":
break
elif cmd == "balance":
print(f"Balance: {account.get_balance()}")
elif cmd in ("deposit", "withdraw"):
try:
amount = float(input("Enter amount: "))
if cmd == "deposit":
account.deposit(amount)
else:
account.withdraw(amount)
print(f"New balance: {account.get_balance()}")
except ValueError as e:
print(f"Error: {e}")
except RuntimeError as e:
print(f"Error: {e}")
except Exception as e:
print(f"Unexpected error: {e}")
else:
print("Invalid command.")
print("Thank you for using our banking system.")
if __name__ == "__main__":
main()
3. The following code is buggy and has poor exception handling. Identify the problems and rewrite it following best practices.
try:
filename = input("Enter filename: ")
with open(filename, "r") as file:
data = file.read()
numbers = data.split()
total = 0
for n in numbers:
total += int(n)
print("Average:", total / len(numbers))
except:
print("Error")
List the problems and provide a corrected version.
Problems identified:
except: catches everything, including KeyboardInterrupt and SystemExit.len(numbers) could be zero, causing ZeroDivisionError that is caught by the bare except, hiding the true problem.int(n) might raise ValueError if a token isn't a number; this also gets swallowed.FileNotFoundError or PermissionError.Corrected version:
def process_data():
try:
filename = input("Enter filename: ")
try:
with open(filename, "r") as file:
data = file.read()
except FileNotFoundError:
print(f"Error: File '{filename}' not found.")
return
except PermissionError:
print(f"Error: Permission denied to read '{filename}'.")
return
except OSError as e:
print(f"Error: OS error reading file: {e}")
return
numbers = data.split()
if not numbers:
print("The file is empty or contains no data.")
return
total = 0
count = 0
for n in numbers:
try:
total += int(n)
count += 1
except ValueError:
print(f"Warning: '{n}' is not a valid integer; skipping.")
if count == 0:
print("No valid numbers found.")
return
average = total / count
print(f"Average of {count} numbers: {average}")
except Exception as e:
print(f"An unexpected error occurred: {e}")
raise # Optionally re-raise for debugging
process_data()
4. Write two versions of a function write_to_file(filename, content) that writes content to a file and ensures the file is closed.
try...finally with explicit open and close.with statement.Explain which version is preferred and why.
Version 1 (try...finally):
def write_to_file_v1(filename, content):
file = None
try:
file = open(filename, "w")
file.write(content)
except Exception:
print("Error writing to file")
raise
finally:
if file:
file.close()
Version 2 (with statement):
def write_to_file_v2(filename, content):
try:
with open(filename, "w") as file:
file.write(content)
except Exception:
print("Error writing to file")
raise
Explanation: Version 2 is preferred because:
with statement automatically handles closing the file, even if an exception occurs.5. Discuss the role of exception handling in software development. Include the following points:
try...except and using conditional checks (e.g., if statements) to avoid exceptions.Provide examples to illustrate your points.
Exception handling is a cornerstone of professional software development, serving as the primary mechanism for managing errors and unexpected conditions. Its importance cannot be overstated—users expect applications to behave predictably, and a program that crashes abruptly provides a poor experience. Graceful error recovery allows programs to handle problems like missing files, invalid user input, or network failures without terminating, instead presenting helpful error messages and allowing users to correct their actions.
The balance between catching exceptions early versus letting them propagate is a key design consideration. The principle is to catch exceptions at the level where you can meaningfully handle them. Low-level functions should not catch exceptions they cannot resolve; instead, they should let them propagate to higher levels where the appropriate recovery strategy is known. For example, a file-reading function might catch FileNotFoundError and attempt to create a default file, while a function that calls it might catch a broader RuntimeError and log it for debugging.
When it comes to using exceptions for control flow, the consensus is clear: exceptions should be used for exceptional conditions, not for normal program flow. Using exceptions to control regular logic—such as using a try...except to check if a key exists in a dictionary instead of using get() or if key in dict—is considered an antipattern. Exceptions are computationally expensive when raised, and they make the code harder to read and maintain. Conditionals are more efficient and clearer when the condition is expected.
The trade-off between using try...except and conditional checks is nuanced. For conditions that are rare and truly exceptional (e.g., file not found when it should exist), try...except is appropriate because it keeps the normal code path clean and separates error handling from business logic. For conditions that are common and expected (e.g., checking if a list is empty before accessing an element), conditionals are preferred because they are clearer and more performant. In practice, the best approach is to use conditionals for expected conditions and exceptions for unexpected ones, striking a balance that maximizes both code clarity and performance.
Examples:
int(input()) – user entering non-numeric is exceptional.try: value = my_dict[key] except KeyError: ... – better to use if key in my_dict or my_dict.get(key, default).This tutorial provides a comprehensive understanding of exception handling with try...except. The quizzes, exercises, and homework problems will help you master these concepts and apply them in real-world scenarios. Happy coding!