Errors and Exceptions in Python
Errors and Exceptions in Python
“If debugging is the process of removing software bugs, then programming must be the process of putting them in.”
― Edsger W. Dijkstra
In your adventures writing code, you will inadvertently introduce bugs to the code or deal with code that has bug(s). Thus debugging, will be a regular process of development. To effectively debug Python code, we need to understand the different kinds of errors we might encounter.
Syntax errors and Exception errors
In Python, errors interrupt the running of a program. They can be broadly grouped into:
- Syntax/ Parsing Errors
- Exceptions
Syntax/ Parsing Errors
These occur when the parser detects incorrect syntax. See the example below:
Here, we forgot to add a colon at the end of the for statement. The caret (^) points to the earliest point where the parser encountered the issue.
Exceptions
Correct syntax doesn't exempt your code from errors during runtime. Python has several built-in exceptions for various runtime errors, and it also allows users to create custom exceptions by subclassing the Exception
class.
When an error occurs within a try block, Python looks for a corresponding except
block to handle it. If no such block is found, then it is an unhandled exception, and the program will terminate with an error message, which includes a stack traceback and exception details
Here's a brief overview of Python's exception hierarchy and the purpose of some base exception classes:
-
BaseException: This is the top-most base class for all built-in exceptions in Python. It is not intended to be directly inherited by user-defined exceptions. Instead, it serves as the base for system-exit exceptions and other built-in exceptions, such as
SystemExit
,KeyboardInterrupt
, andGeneratorExit
. Let us check that they indeed inherit fromBaseException
. If our condition fails (the check is whether an exception class is a child ofBaseException
), fails, anAssertError
will be raised (bad), else the code will run silently (good). -
Exception: This is the standard base class for most built-in exceptions and is the superclass from which user-defined exceptions are typically derived. It is subclassed by all exceptions that are meant to be caught and handled within user programs.
-
ArithmeticError: This class serves as the base for built-in exceptions triggered by arithmetic errors like overflow or division by zero. It encompasses specific subclasses such as
OverflowError
,ZeroDivisionError
, andFloatingPointError
. -
BufferError: This error is raised when a buffer related operation cannot be performed. Buffers are sequences of objects laid out in memory, and
BufferError
is related to issues that arise when managing or accessing these. -
LookupError: This class is the base class for exceptions triggered when a key or index used on a mapping or sequence turns out to be invalid. It has subclasses such as
KeyError
when a key is not found in a dictionary, andIndexError
for when an index is not within the bounds of a list.
Improved Error Messages in Python 3.12
Python 3.12 introduced improved error messages that can make debugging easier. Let's review the changes by comparing messages between Python 3.10 and 3.12:
Let's compare error messages between Python versions 3.10 and 3.12 to see what I mean.
Exhibit A:
Calling a function from the math module without importing it. This will surely raise a NameError
exception.
In Python 3.12, modules from the standard library are suggested as part of the error message.
Python 3.12
Output
Python 3.10
Output
Exhibit B:
You normally write TypeScript but need to whip up some Python. So you (wrongly) write an import statement.
Python 3.12
Output
Python 3.10
Output
Some common exceptions you may encounter
Python's built-in exceptions cover a wide range of error situations. Here's an overview of some common ones you may encounter (examples have been tested with Python 3.12.0):
AssertionError:
Raised when an assert statement fails. See the example below:
Output
In the above example, an AssertionError
is raised because 2 is not equal to 3.
AttributeError
Raised when an attribute reference or assignment fails. See the example below:
Output
In the above example, an AttributeError is raised when we try to reference a non-existent attribute (email) on the dict object.
EOFError
Raised when input()
reaches an end-of-file condition without
reading any data.
Type the line data = input("Enter your name: ")
on the python prompt.
Press Ctrl+d and see what happens. An EOFError should be raised as shown below:
Output
This is because pressing Ctrl+d forced python to find an end-of -file.
ModuleNotFoundError
Subclass of ImportError
. Raised when an imported module could not be located.
See the example below:
Output
This is because Python could not find the module bbfbfm
as it does not exist.
IndexError
Raised when the index of a sequence is out of range. That is, the index is equal to or greater than the length of the sequence.
See the example below:
Output
The above list has 4 elements, we tried to acccess the fifth element thus raising an IndexError.
IndentationError
Subclass of syntax error and is a base class for syntax errors caused by incorrect indentation.
See the example below:
In the above example, the second line ought to have been indented.
KeyError
Raised when a key is not found in a dictionary's set of keys.
See the example below:
Output
In the above example, KeyError was raised because we accessed a non-existent key (email) on the person dictionary. If we want to access a key that may not be present in a dict but do not want to raise a KeyError
exception, we can use the get
method, available in Python dictionaries. dict.get
accepts two arguments, the key to lookup in the dict, and an optional value which is to be returned if the key does not exist. If we do not provide a default value and the key does not exist, the method returns None
without raising an exception. The example above could be redone as follows.
Output
KeyboardInterrupt
Raised when the user hits either ctrl-c or delete.
Let's simulate a long running operation by using time.sleep
. time.sleep
delays execution for the given number of seconds.
As the execution is delayed, hit ctrl-c and see what happens.
Output
The execution should stop and a KeyboardInterrupt
thrown.
NameError
Raised when a local or global name is not found.
See the example below:
Output
In the above example, email
was referenced before being assigned.
FileNotFoundError
Raised when an attempt is made to read from a non -existent file or directory.
See the example below:
Output
In the example above, we tried opening a file that does not exist.
OverflowError
In Python, an OverflowError
is a specific kind of ArithmeticError
that occurs when a calculation exceeds the maximum limit for a numeric type (e.g., integers, floating numbers). However, it is a rare exception in Python 3 because the int type is unbounded. The most common occurrence is with floating-point numbers or functions that operate on them, such as exponentiation.
Raised when the result of an arithmetic operation is too large to be represented by the data type being used, particularly for integers (int
) and floating-point numbers (float
).
See the example below:
Output
The result of raising math.pi to the power of 10000000 was too large hence the OverflowError.
RecursionError
Raised when interpreter detects that the maximum recursion depth has been exceeded.
See the example below:
Output
StopIteration:
This is raised by next()
and an iterator's __next__()
method to tell that
there are no more items to be produced by the iterator.
See the example below:
Output
SyntaxError
Raised when the parser encounters incorrect syntax.
Output
TypeError
Raised when an operation is performed to an object of an inappropriate type. For example passing in arguments of the wrong type.
See the example in the snippet below:
str.capitalize() expects a string but we passed in a different type, an int.
UnboundLocalError
Subclass of NameError. Raised when a local variable is referenced in a function or method, but no value has been bound to that variable.
See the example snippet below:
Output
The above happened because the variable age
changed scope. The line age += 1
causes a new assignment and this makes age
local to the function scope
, consequently shadowing age
in the outer scope. So when the call to print (age)
happens, we have a problem since we are referencing a variable that
has not been assigned. This is because Python now considers age
local to
increment_age()
.
Read more on UnboundLocalError here
ValueError
Raised when an operation/function gets an argument of the right type but the wrong value.
See the example below:
Output
In the example above, math.sqrt
got an argument of the correct type (int
) but had the wrong value (negative integer)
ZeroDivisionError
Raised when division by zero occurs.
See the example below:
Output
In the above example, ZeroDivisionError
was raised by dividing 200 with zero.
Handling errors
Try
A try
block is used to wrap potentially problematic code. An except
block then catches specific exceptions and handles them accordingly. You can have multiple except
statements or handle a group of exceptions.
Else Clause
The else clause in a try-except block executes if no exceptions are raised in the try block, making it a good place for code that should run only when the try block is successful. That is, no exceptions were raised.
The Finally Clause
The finally clause executes regardless of whether an exception occurred, which is ideal for cleanup actions such as closing files or releasing resources.
Creating Custom Exceptions
Raising exceptions
You may want to raise an exception. Consider the following add
function that only accepts numbers.
Conclusion
We went over a few base classes, buitlin exceptions and how we can handle them. As you saw, error messages in Python 3.12 are more descriptive. Hopefully, we now have a better understanding of Python exceptions and how to handle them.