Exception Handling: try and except Blocks Guide
In real-world programs, things go wrong. Network connections fail, files are moved, users enter invalid data, and external APIs return unexpected responses. Anticipating these failures and handling them gracefully separates production-quality code from scripts that crash unexpectedly. Exception handling—the process of catching and responding to runtime errors—is not optional; it is the foundation of robust, reliable software. The primary tool in Python is the try...except block, which lets you anticipate errors and define how your program should recover.
Key Takeaways
- Exceptions are runtime errors that disrupt normal program flow if not caught; syntax errors are caught before execution.
tryblocks contain code that might fail;exceptblocks define recovery logic for specific error types.- Catch specific exceptions (
ValueError,FileNotFoundError,TypeError) rather than bareexcept:to avoid hiding bugs. - Raise your own exceptions to signal logical errors in your program's flow using
raise ExceptionType("message"). - The
as esyntax captures the exception object, allowing you to access its error message and other attributes.
What Is an Exception and Why Should You Handle It?
An exception is an error that occurs during the execution of a program. It is different from a syntax error, which is a mistake in code structure (like a typo or missing colon) that Python's parser detects before the program runs. Syntax errors prevent execution entirely; exceptions occur while the program is running and, if unhandled, cause the program to terminate and print a traceback.
When Python encounters an unhandled exception, it stops executing code and displays a traceback—a sequence of function calls showing where the error occurred and how the program got there. Traceback messages often point to the true root cause, but reading them takes practice.
# This raises a ZeroDivisionError (exception)
numerator = 10
denominator = 0
result = numerator / denominator # Error: division by zero
# Traceback shows: ZeroDivisionError: division by zero
Common built-in exceptions you will encounter:
ValueError: An operation receives an argument of the correct type but an invalid value (e.g.,int('abc')).TypeError: An operation is applied to an object of an inappropriate type (e.g.,'hello' + 5).ZeroDivisionError: Occurs when dividing by zero.FileNotFoundError: Raised when you try to open a file that does not exist.IndexError: Raised when accessing a list index that is out of bounds (e.g.,my_list[99]when the list has 10 items).KeyError: Raised when accessing a dictionary key that does not exist (e.g.,my_dict['missing_key']).AttributeError: Raised when accessing a nonexistent attribute on an object (e.g.,obj.nonexistent_method()).ImportError: Raised when a module cannot be imported (e.g.,import nonexistent_module).
Each exception is a class that inherits from the base Exception class. This inheritance hierarchy allows you to catch a broad category of errors or a specific error type.
How Do You Use the try...except Block?
The try...except block is the mechanism for catching exceptions. Code inside the try block is executed. If an exception occurs, Python stops executing the try block and jumps to the except block. If no exception occurs, the except block is skipped.
The syntax:
try:
# Code that might cause an exception
...
except ExceptionType:
# Code that runs ONLY if an exception of type 'ExceptionType'
# occurred in the try block
...
Handling a Specific Exception
numerator = 10
denominator = 0
try:
result = numerator / denominator
print(f"The result is {result}")
except ZeroDivisionError:
print("Error: Cannot divide by zero. Please provide a non-zero denominator.")
print("The program continues to run after the error.")
Output:
Error: Cannot divide by zero. Please provide a non-zero denominator.
The program continues to run after the error.
Instead of crashing, the program caught the specific error, printed a helpful message, and continued executing. The print statement after the try...except block ran normally, demonstrating that the program recovered from the error.
Capturing the Exception Object with as
The as e syntax assigns the exception object to a variable, allowing you to access its message and other attributes inside the except block:
try:
age = int("not a number")
except ValueError as e:
print(f"Caught an error: {e}")
print(f"Error type: {type(e)}")
Output:
Caught an error: invalid literal for int() with base 10: 'not a number'
Error type: <class 'ValueError'>
The exception object e contains the error message and additional context. Accessing e lets you provide more informative error messages to users or log detailed error information.
Multiple except Blocks for Different Exceptions
A single try block can have multiple except blocks, each catching a different exception type. Python checks each except in order and executes the first one that matches.
def convert_user_age(age_str):
"""Convert a string to an integer age."""
try:
age = int(age_str) # Can raise ValueError
if age < 0:
raise ValueError("Age cannot be negative.")
return age
except ValueError as e:
print(f"ValueError: {e}")
return None
except TypeError as e:
print(f"TypeError: {e}")
return None
print(convert_user_age("thirty")) # Output: ValueError: invalid literal...
print(convert_user_age(-5)) # Output: ValueError: Age cannot be negative.
print(convert_user_age(30)) # Output: 30
How Do You Catch Specific Exceptions vs. All Exceptions?
It is a critical best practice to catch specific exceptions whenever possible. While you can use a bare except: (with no exception type) to catch any exception, this is dangerous and hides bugs.
Why catch specific exceptions:
- Clarity: Your code explicitly documents what errors you anticipate and how you handle each one.
- Debugging: If an unexpected error occurs, the program will crash and print a traceback, helping you find the bug rather than silently failing.
- Correctness: Different errors may require different recovery strategies. Catching a
ValueError(bad input) is different from catching aFileNotFoundError(missing file).
# Bad: bare except catches everything, including unexpected errors
try:
result = numerator / denominator
except: # This catches even KeyboardInterrupt, SystemExit, etc.
print("Something went wrong")
# Good: catch specific exception
try:
result = numerator / denominator
except ZeroDivisionError:
print("Cannot divide by zero.")
A Practical Example: Robust User Input
def get_user_age():
"""Prompt user for age with error handling."""
try:
age_str = input("Please enter your age: ")
age = int(age_str) # Can raise ValueError
if age < 0:
raise ValueError("Age cannot be negative.")
if age > 150:
raise ValueError("Age seems unrealistic.")
return age
except ValueError as e:
print(f"Invalid input: {e}. Please enter a valid positive age under 150.")
return None
age = get_user_age()
if age is not None:
print(f"You will be {age + 1} on your next birthday.")
else:
print("Failed to get a valid age.")
How it works:
- The code inside the
tryblock is executed. - If the user enters "abc",
int('abc')raises aValueError. Theexceptblock catches it and prints an error message. - If the user enters "-10", the code detects it and explicitly
raiseaValueErrorwith a custom message. - If the user enters a valid age like "30", no exception occurs, and the
exceptblock is skipped entirely. - The
as esyntax assigns the exception object toe, allowing us to access its message viastr(e).
How Do You Raise Your Own Exceptions?
Beyond catching exceptions that Python raises, you can raise your own exceptions to signal that something has gone wrong in your program's logic. Use the raise keyword followed by an exception type and a message:
def validate_email(email):
"""Validate email format."""
if "@" not in email:
raise ValueError("Email must contain an @ symbol.")
if email.endswith("."):
raise ValueError("Email cannot end with a period.")
return email
try:
email = validate_email("user.domain.")
except ValueError as e:
print(f"Validation failed: {e}")
Raising exceptions allows you to enforce business logic constraints. For example, a bank transfer function might raise a ValueError if the amount is negative, making it explicit that invalid transfers are not silently ignored.
def transfer_funds(from_account, to_account, amount):
"""Transfer funds between accounts."""
if amount <= 0:
raise ValueError("Transfer amount must be positive.")
if from_account.balance < amount:
raise ValueError("Insufficient funds.")
from_account.balance -= amount
to_account.balance += amount
return True
try:
transfer_funds(alice_account, bob_account, -100)
except ValueError as e:
print(f"Transfer failed: {e}")
Frequently Asked Questions
What is the difference between ValueError and TypeError?
ValueError is raised when an operation receives an argument of the correct type but an invalid value. For example, int('abc') raises ValueError because 'abc' is a string (correct type) but cannot be converted to an integer. TypeError is raised when an operation is applied to an object of the wrong type. For example, 'hello' + 5 raises TypeError because you cannot add a string and an integer. ValueError is about the value being invalid; TypeError is about the type being wrong.
Should I catch exceptions or let them bubble up?
Catch exceptions only when you can handle the error meaningfully—either by recovering (retrying, using a default value) or providing user-friendly feedback. If you cannot handle an error, let it propagate to the caller. Catching and silently ignoring exceptions hides bugs and makes debugging difficult. For example, in a web service, catch FileNotFoundError and return a 404 response; but if a database query fails unexpectedly, let that exception bubble up so operators see the error in logs.
What is the difference between raise and assert?
raise explicitly throws an exception with a message, used for error handling in production code. assert is a debugging tool that checks a condition and raises AssertionError if it is false. Assertions can be disabled globally with Python's -O flag (optimization mode), so never use assert for logic that must always run. Use raise ValueError(...) for production validation; use assert for sanity checks during development.
Can I have multiple lines in an except block?
Yes, the except block can contain any number of statements, as long as they are indented at the same level. For example, you might log the error, update a status variable, and clean up resources.
try:
result = risky_operation()
except Exception as e:
print(f"Error occurred: {e}")
log_error_to_file(e)
cleanup_resources()
return None
What is the finally block?
The finally block executes regardless of whether an exception occurred. It is used for cleanup: closing files, releasing database connections, or logging. If you have try, except, and finally, the finally block always runs, even if the except block returns early or raises a new exception. For example: try: f = open(file) ... finally: f.close() ensures the file is closed even if an error occurs.