Exceptions are an important aspect of programming in Python that every developer needs to understand. At its core, an exception is an error that occurs during the execution of a program. Unlike syntax errors that prevent a program from running, exceptions arise at runtime and cause the program to stop its normal execution. Handling exceptions properly is essential to make Python programs robust and prevent crashes.
This comprehensive guide will explain what exceptions are, how they work, and how to handle them properly in Python. We will cover the following topics in depth with example code snippets:
Table of Contents
Open Table of Contents
What are Exceptions?
Exceptions, as the name suggests, are events that occur when some kind of exceptional condition arises in a program. In Python, exceptions are errors that are detected during execution and result in the program stopping normal execution.
Some common examples of exceptions:
- Trying to open a file that doesn’t exist
- Accessing an index out of range for a list
- Dividing by zero
- Making an invalid type conversion
When an exception occurs, Python generates an exception object that contains information about the error. This exception object is passed to the exception handler if the programmer has set one up using try
and except
blocks (covered later). Otherwise, the default exception handler is called which stops the execution of the program and prints an error traceback.
The point to note is that the program does not stop execution immediately on an error. The exception mechanism allows you to intercept errors and handle them gracefully so the program can continue normal execution. This makes programs more robust and fault-tolerant.
The Exception Hierarchy
All built-in exceptions in Python inherit from a base class called BaseException
. This forms a hierarchy of exception classes that inherit from each other.
The highest-level distinction is between exceptions that inherit from Exception
and those that inherit directly from BaseException
.
For most practical purposes, you need only concern yourself with subclasses of Exception
. The subclasses of BaseException
are either internal to Python or deprecated.
Here is a simplified view of the exception inheritance hierarchy:
BaseException
+-- SystemExit
+-- KeyboardInterrupt
+-- GeneratorExit
+-- Exception
+-- StopIteration
+-- StopAsyncIteration
+-- ArithmeticError
| +-- FloatingPointError
| +-- OverflowError
| +-- ZeroDivisionError
+-- AssertionError
+-- AttributeError
+-- BufferError
+-- EOFError
+-- ImportError
+-- LookupError
| +-- IndexError
| +-- KeyError
+-- MemoryError
+-- NameError
| +-- UnboundLocalError
+-- OSError
| +-- BlockingIOError
| +-- ChildProcessError
| +-- ConnectionError
| | +-- BrokenPipeError
| | +-- ConnectionAbortedError
| | +-- ConnectionRefusedError
| | +-- ConnectionResetError
| +-- FileExistsError
| +-- FileNotFoundError
| +-- InterruptedError
| +-- IsADirectoryError
| +-- NotADirectoryError
| +-- PermissionError
| +-- ProcessLookupError
| +-- TimeoutError
+-- ReferenceError
+-- RuntimeError
| +-- NotImplementedError
| +-- RecursionError
+-- SyntaxError
| +-- IndentationError
| +-- TabError
+-- SystemError
+-- TypeError
+-- ValueError
| +-- UnicodeError
| +-- UnicodeDecodeError
| +-- UnicodeEncodeError
| +-- UnicodeTranslateError
+-- Warning
+-- DeprecationWarning
+-- PendingDeprecationWarning
+-- RuntimeWarning
+-- SyntaxWarning
+-- UserWarning
+-- FutureWarning
+-- ImportWarning
+-- UnicodeWarning
+-- BytesWarning
+-- ResourceWarning
This hierarchy helps in catching specific or general exceptions based on the level of granularity you need in handling different errors.
For example, a ZeroDivisionError
is more specific than an ArithmeticError
, but catching the ArithmeticError
will catch both kinds of errors.
Raising Exceptions
Python raises exceptions automatically when errors occur at runtime. But you can also manually raise exceptions if needed.
Raising an exception is done by using the raise
statement.
raise ExceptionClass(args)
The ExceptionClass
is the type of exception you want to raise, and args
is optional information on the exception being raised.
For example:
# Raise a generic Exception
raise Exception('An error occurred')
# Raise a TypeError
raise TypeError('Invalid type for variable a')
# Raise a custom exception
class MyCustomError(Exception):
pass
raise MyCustomError('Custom error message')
You can raise exceptions in situations where you want to force a certain condition to be met or explicitly fail if something unexpected occurs.
Some common use cases for raising exceptions:
- Validating input data and failing if constraints are not met
- Indicating errors in business logic conditions
- Signaling failure in a library, module or package
- Throwing an exception when an invalid state is encountered
Raising an exception will cause the current execution to stop and the exception will propagate up the call stack until it is caught or causes the program to exit.
Handling Exceptions
Now that we know how exceptions work in Python, let’s see how to handle them effectively.
The primary way to handle exceptions in Python is to use try
and except
blocks. This allows you to intercept exceptions and take corrective actions instead of letting them crash the program.
The try and except Block
The basic syntax for using try and except blocks is:
try:
# Normal code here
pass
except:
# Execute this code when an exception occurs
pass
The code inside the try
block is executed normally until an exception occurs. Once an exception is encountered, the remaining code in the try
block is skipped and the except
block is executed to handle the exception.
For example:
try:
file = open('data.txt')
data = file.read()
print(data)
except:
print('Could not open file')
This avoids the program crashing if the file cannot be opened by intercepting the FileNotFoundError
and handling it gracefully.
You can have multiple except
blocks to handle different types of exceptions in different ways. The exception will be matched from top to bottom and the first block that matches will be executed.
A common practice is to make the last except
block a bare except
to catch any unanticipated exceptions.
Catching Specific Exception Types
A better way is to catch and handle specific exception types instead of generic Exception
class.
For example:
try:
# Code here
except TypeError:
# Handle TypeError
except ValueError:
# Handle ValueError
except OSError:
# Handle OSError
except:
# Handle any other exceptions
This allows you to selectively handle different exception conditions.
You can even store the exception instance in a variable to get attributes like the error message:
try:
# Code
except Exception as ex:
print('Error occurred:', ex.args)
Multiple exceptions can be handled together by specifying them in a tuple:
except (TypeError, ValueError):
# Handle TypeError and ValueError
The else Clause
You can use an optional else
clause after all except
blocks. The code in else
will only execute if no exceptions were raised in the try
block:
try:
# Code
except:
# Exception occurred
else:
# No exceptions
pass
This can be useful to run code that should only execute if everything in the try
block succeeded.
finally Clause
The finally
clause can be used at the end to define a block of code to execute regardless of whether an exception occurred.
For example:
try:
file = open('data.txt')
# Process file
except OSError:
print('Could not open file')
finally:
file.close() # Close file even if exception
The finally
block runs after any except
blocks but before control reaches the rest of the program outside the try
statement.
This is commonly used to perform cleanup actions like closing files, releasing resources etc.
The with Statement
A simpler way to ensure resources like files are properly handled is to use the with
statement.
The advantage of using a with
block for resource handling is that it neatly encapsulates setup and teardown actions within a single block.
The syntax for with
is:
with EXPRESSION as VAR:
# Code here
Where:
EXPRESSION
is the resource you want to open and manageVAR
is the variable to assign the opened resource object to
Internally, the EXPRESSION
is evaluated to obtain a resource known as a context manager. The context manager handles opening and closing the resource.
For example, to safely open and close a file:
with open('data.txt') as file:
data = file.read()
# File closed automatically here
This avoids having to explicitly close the file even if exceptions occur. The with
block allows you to get the benefits of using try
/finally
in a simpler way.
Custom Exceptions
In addition to built-in exceptions, you can define your own custom exception classes in Python to indicate specific error conditions relevant to your program.
Creating a custom exception is as simple as defining a new class inheriting from Exception
:
class MyCustomError(Exception):
pass
raise MyCustomError('Custom exception raised')
You can define additional attributes and methods to provide more context on the exception:
class InvalidInputError(Exception):
"""Exception raised for invalid input
Attributes:
expression -- input expression that caused the error
message -- explanation of the error
"""
def __init__(self, expression, message):
self.expression = expression
self.message = message
raise InvalidInputError('2 + cat','Invalid expression: '+expression)
Now users of your library can specifically catch this InvalidInputError
instead of generic exceptions.
Defining custom exceptions is useful in making your code more readable, maintainable and user-friendly.
Best Practices for Exceptions
Here are some best practices to keep in mind when dealing with exceptions in Python:
-
Avoid broad
except
clauses that catch all exceptions. Be as specific as possible when handling expected error conditions. -
Document all exceptions that can be raised from a function or module using docstrings and comments.
-
Only catch exceptions that you know how to handle. Propagate unexpected ones to the caller.
-
Print the exception traceback for debugging but provide a user-friendly error message in production.
-
Use custom exception classes to indicate specific error conditions relevant to your domain.
-
Perform cleanup actions like closing files and releasing resources in a
finally
block. -
Use the
with
statement for resource handling instead of explicittry
/finally
. -
Raise exceptions at the source of the error rather than letting issues propagate up.
-
Limit use of bare
raise
statements to reraising caught exceptions.
Conclusion
Understanding exceptions is crucial to developing robust programs in Python. Exceptions provide an organized way to react to unexpected runtime problems and prevent the program from terminating abruptly.
Using the exception handling tools like try
, except
, finally
and with
allows you to intercept errors and recover gracefully instead of letting them crash your program. Defining custom exceptions also improves the usability of your code.
By following best practices in exception handling, you can write Python code that is resilient in the face of failures and provides clear error reporting. This results in applications that remain stable and reliable even when things go wrong.