Table of Contents
- Table of Contents
- Introduction
- Common Exceptions
- Handling Exceptions
- Including an Else Clause
- Adding a Finally Clause
- Raising Exceptions
- Adding More Information to Exceptions
- User-Defined Exceptions
- Summary
Introduction
As you write code, errors may occur due to wrong syntax or program execution. The former is known as a syntax error, the latter exception. An unhandled exception will cause your program to crash. Therefore, it is necessary to understand and handle them.
This article focuses on exceptions. Syntax errors are already visible mistakes.
Common Exceptions
Some exceptions are visible to the user. These include NameError
, KeyError
, IndexError
, and so on. Other exceptions are not. These include but are not limited to StopIteration
and GeneratorExit
exceptions.
Below are some of the exceptions you are likely to encounter.
AssertionError
When an assert
statement fails, it raises an assertion error. For example:
assert (4 + 5) == 8
Traceback (most recent call last):
File "C:\articles\python\introduction\codes\exceptions.py", line 1, in <module>
assert (4 + 5) == 8
AssertionError
You can customize the message associated with an assertion error. For example:
assert 7 == 8, 'Not sure 7 is equal to 8'
Traceback (most recent call last):
File "C:\articles\python\introduction\codes\exceptions.py", line 3, in <module>
assert 7 == 8, 'Not sure 7 is equal to 8'
AssertionError: Not sure 7 is equal to 8
Use the assert statement to ensure your function meets a particular condition during unit testing with a package such as pytest.
AttributeError
Attribute errors occur when you try to access a property (or method) that does not exist in an object. For example:
class Person:
pass
person = Person()
person.age
Traceback (most recent call last):
...
AttributeError: 'Person' object has no attribute 'age'
Another example
# Empty dictionary
records = {}
# Works! items() method exist
records.items()
# Throws as AttributeError exception
records.age()
Traceback (most recent call last):
...
AttributeError: 'dict' object has no attribute 'age'
Ensure you are using attributes that exist to avoid this error.
ImportError
An import error usually occurs when you try to import a non-existent variable, function, or module from another module. For example, try to import Fish
from the abstract base class (abc
) module:
from abc import Fish
...
ImportError: cannot import name 'Fish' from 'abc'
...
ModuleNotFoundError
A ModuleNotFoundError
is a subclass of ImportError
. When a module does not exist or is not reachable, Python raises a ModuleNotFoundError
. For example, I don't have a module called sky
:
import sky
...
ModuleNotFoundError: No module named 'sky'
The solution is to ensure that a file sky.py
exists within your project directory.
IndexError
An IndexError
is raised when you supply a sequence (e.g. list, tuple, etc.) with an index that is out of range. Slice indices are silently truncated to fall in the allowed range. For example:
favorites = ['Peter', 'Stewie', 'Brian', 'Quagmire', 'Joe']
# These works
print(favorites[1]) # Stewie
print(favorites[1:2000])
print(favorites[1000:2000]) # []
# This fails: index is from 0 - 4
print(favorites[10])
Stewie
['Stewie', 'Brian', 'Quagmire', 'Joe']
[]
Traceback (most recent call last):
...
IndexError: list index out of range
KeyError
A KeyError occurs when you try to access a dictionary key that does not exist. For example:
oldest_person = { 'name': 'Methuselah Doe', 'age': 1000 }
# Ok!
print(oldest_person['name'])
# Oops!
print(oldest_person['occupation'])
Traceback (most recent call last):
...
KeyError: 'occupation'
To avoid KeyError
, use the dictionary's .get()
method. get()
returns None
for a non-existent key. For example:
print(oldest_person.get('occupation')) # None
Also, .get()
optionally takes a default value that gets returned when the key does not exist. For example:
print(oldest_person.get('occupation', 'Musician')) # Musician
KeyboardInterrupt
When you hit the interrupt key (Control-C or Delete), Python raises a keyboardinterrupt exception. For example:
# Simulate a long-running program
import time
while True:
print(time.time())
# add some delay
time.sleep(1)
1707814961.0006871
1707814962.0126708
1707814963.0137038
1707814964.014804
Traceback (most recent call last):
...
KeyboardInterrupt
^C
NameError
Python raises a NameError
when you use a global or local name or variable (defined within a function) that you have not created. For example:
print(favorite_food)
Traceback (most recent call last):
...
NameError: name 'favorite_food' is not defined
The solution is to ensure you define a variable before using it. For example:
favorite_food = "Whatever works!"
# Works!
print(favorite_food)
IndentationError
An indentation error is a syntax error that occurs when your code is not properly indented. Thankfully, most code editors will help you avoid this kind of error. In the example below, I inserted two spaces before calling print()
print('Hello')
# IndentationError: unexpected indent
TypeError
A TypeError
occurs when you perform an operation or apply a function to an object of inappropriate type. For example, summing two dictionaries or converting an inappropriate string to an integer will raise a TypeError
.
class Person:
pass
# Fails!
map(lambda x: x * 2, Person())
# TypeError: 'Person' object is not iterable
# Fails!
print({} + {})
# TypeError: unsupported operand type(s) for +: 'dict' and 'dict'
ValueError
A ValueError
exception occurs when an operation or function receives an argument with the right type but an inappropriate value. A classic example is when you try to convert an inappropriate string to an integer.
print(int('abc'))
# ValueError: invalid literal for int() with base 10: 'abc'
ZeroDivisionError
A ZeroDivisionError
exception occurs when you attempt to divide (or modulo) a value by zero. For example:
print(4 / 0)
# ZeroDivisionError: division by zero
print(3 % 0)
# ZeroDivisionError: integer modulo by zero
The base class of ZeroDivisionError
is the ArithmeticError
. The ArithmeticError
is also the parent class of OverflowError
, an exception raised when the result of an arithmetic operation is too large to be represented.
File Errors
Other exceptions you might encounter include:
-
FileExistsError
: Occurs when trying to create a file or directory which already exists -
FileNotFoundError
: Raised when a file or directory is requested but doesn’t exist.
with open('hello.txt', mode='rt', encoding='utf-8') as f:
print(f.read())
# Side note: The program never gets here
print('I will never get here if hello.txt does not exist')
...
FileNotFoundError: [Errno 2] No such file or directory: 'hello.txt'
Do check the Python documentation for more types of exceptions.
Handling Exceptions
To prevent your program from crashing you need to handle possible exceptions. In Python, you use the try-except
statement to handle exceptions. A classic example is asking for a user's age until he provides the expected value:
while True:
try:
age = int(input("Please enter your age: "))
if age >= 18:
break
else:
print("The age you provided is less than 18!")
except ValueError:
print("Oops! That was no valid number. Try again...")
Please enter your age: 12
The age you provided is less than 18!
Please enter your age: weerr
Oops! That was no valid number. Try again...
Please enter your age: 45
Let's handle the FileNotFoundError
you saw earlier.
try:
with open('hello.txt', mode='rt', encoding='utf-8') as f:
print(f.read())
except FileNotFoundError as e:
print(e)
# Our program continues because we handled the exception above
print('I will run even if hello.txt does not exist')
print('Me too!')
[Errno 2] No such file or directory: 'hello.txt'
I will run even if hello.txt does not exist
Me too!
A try
statement can have multiple except
blocks or clauses. Also, an except
clause may have many exceptions in a tuple with parenthesis. Here is a contrived example modified from Python documentation:
def divide(a, b):
return a / b
try:
f = open('myfile.txt')
s = f.readline()
i = int(s.strip())
divide(10, 0)
except OSError as err:
print("OS error:", err)
except ValueError:
print("Could not convert data to an integer.")
except ZeroDivisionError as err:
print("Zero division error:", err)
except Exception as err:
print(f"Unexpected {err=}, {type(err)=}")
You can shorten the code above to
try:
f = open('myfile.txt')
s = f.readline()
i = int(s.strip())
divide(10, 0)
except (OSError, ValueError, ZeroDivisionError) as err:
print(err)
except Exception as err:
print(f"Unexpected {err=}, {type(err)=}")
- A call to
open()
might raise aFileNotFoundError
or any other exception that is a subclass ofOSError
, so we useOSError
as a catch-all for these exception types. - Converting an invalid string to an integer will raise a
ValueError
. We handle that directly. - A call to
divide(10, 0)
raises aZeroDivisionError
exception. We handle the exception. Atry-except
statement can handle any exception raised from a function. - All built-in, non-system-exiting exceptions are subclasses of the
Exception
class, so we handle any other form of exception that might occur. TheException
class derives from theBaseException
class, the mother of all exception classes.
Consider using specific exception types such as FileNotFoundError before using the general ones like OSError.
Including an Else Clause
You can add an optional else clause to the try-except
statement. The else clause must follow all except clauses. Use it for code that must run if the try clause does not raise an exception. For example:
for arg in sys.argv[1:]:
try:
f = open(arg, 'r')
except OSError:
print('cannot open', arg)
else:
print(arg, 'has', len(f.readlines()), 'lines')
f.close()
Using the else clause is better than adding additional code to the try clause because it avoids accidentally catching an exception from another code you did not intend to protect with the try-except
statement.
Adding a Finally Clause
Also, you can include an optional finally
block if you need to run some form of "clean up" code. Unlike the else clause, the code in the finally block will always run regardless of an exception. For example:
def divide(x, y):
try:
result = x / y
except TypeError:
print(f"divide({x}, {y}): You cannot divide {x} by {y}")
except ZeroDivisionError:
print(f"divide({x}, {y}): You cannot divide {x} by zero.")
else:
# Only runs if the try block succeeds
print(f"divide({x}, {y}): result is {result}")
finally:
# Always runs
print("executing finally clause")
- Dividing with unsupported types such as strings will raise a
TypeError
which is handled in the firstexcept
block. Thefinally
block runs. The program ends. - The second
except
block catches aZeroDivisionError
. Thefinally
block runs. The program ends. - The
else
clause runs whendivide()
succeeds. Thefinally
block runs. The program ends.
divide('hello', 'world')
print() # for space
divide(4, 0)
print() # for space
divide(3, 2)
divide(45, 2): You cannot divide hello by world
executing finally clause
divide(4, 0): You cannot divide 4 by zero.
executing finally clause
divide(3, 2): result is 1.5
executing finally clause
Raising Exceptions
There are instances where you want to raise exceptions if a user's input does not meet the condition you have specified. You can do so using the raise
statement. For example:
def divide_only_ints(num1: int, num2: int):
if isinstance(num1, int) and isinstance(num2, int):
print(f'num1 / num2 = {num1 / num2 :.2f}')
else:
raise TypeError(f'num1 and num2 must be ints!\nGot {num1} and {num2} instead')
# Works!
divide_only_ints(4, 3) # num1 / num2 = 1.33
# Fails
divide_only_ints(4.5, 12.3)
Traceback (most recent call last):
...
TypeError: num1 and num2 must be ints!
Got 4.5 and 12.3 instead
raise Exception
is the same asraise Exception()
. You can optionally include a custom message. For example,raise Exception('My custom message')
.
With the raise
keyword, you can re-raise or propagate an exception if you don't want to handle it. For example:
try:
divide_only_ints("a", "b")
except TypeError:
print('An exception occurred! Sending it your way!')
raise
An exception occurred! Sending it your way!
...
TypeError: num1 and num2 must be ints!
Got a and b instead
You can also propagate multiple exceptions from an except
block using a raise
or raise ... from
statement. This propagation is known as exception chaining. For example:
try:
divide_only_ints("a", "b")
except TypeError:
raise ValueError('Just because I can')
Or using raise ... from
:
try:
divide_only_ints("a", "b")
except TypeError as err:
raise ValueError('Just because I can') from err
Traceback (most recent call last):
...
TypeError: num1 and num2 must be ints!
Got a and b instead
During the handling of the above exception, another exception occurred:
Traceback (most recent call last):
...
ValueError: Just because I can
Adding More Information to Exceptions
The section requires Python >= 3.11
Sometimes, you want to add more information to an exception before re-raising it. You can use the exception's .add_notes()
method for such. For example:
try:
divide_only_ints("a", "b")
except TypeError as err:
err.add_note('I used the divide_only_ints() function')
err.add_note('The left operand was the letter a')
err.add_note('The right operand was the letter b')
print('An exception occurred! So Sending it your way!')
raise
An exception occurred! So Sending it your way!
Traceback (most recent call last):
...
TypeError: num1 and num2 must be ints!
Got a and b instead
I used the divide_only_ints() function
The left operand was the letter a
The right operand was the letter b
User-Defined Exceptions
Finally, you can define your exceptions by deriving from the Exception class directly or indirectly. For example:
class DivideByIntOnlyError(Exception):
"""An example of a user-defined exception."""
def __init__(self, *args: object) -> None:
super().__init__(*args)
def add_note(self, __note: str) -> None:
print('About to start adding notes for useless logging purposes')
return super().add_note(__note)
def divide_only_ints(num1: int, num2: int):
if isinstance(num1, int) and isinstance(num2, int):
print(f'num1 / num2 = {num1 / num2 :.2f}')
else:
raise DivideByIntOnlyError(
f'num1 and num2 must be ints!\nGot {num1} and {num2} instead')
try:
divide_only_ints("a", "b")
except DivideByIntOnlyError as err:
err.add_note('I used the divide_only_ints() function')
err.add_note('The left operand was the letter a')
err.add_note('The right operand was the letter b')
print('An exception occurred! So Sending it your way!')
raise
About to start adding notes for useless logging purposes
About to start adding notes for useless logging purposes
About to start adding notes for useless logging purposes
An exception occurred! So Sending it your way!
Traceback (most recent call last):
...
DivideByIntOnlyError: num1 and num2 must be ints!
Got a and b instead
I used the divide_only_ints() function
The left operand was the letter a
The right operand was the letter b
Note that overriding the
__init__()
andadd_note()
methods withinDivideByIntOnlyError
is unnecessary in most cases. I've included it here for demo purposes only.
Also note that While you can defineException
classes to do anything any other class can, it is best to keep it simple.
Summary
In this article, you saw examples of exceptions like IndexError
and KeyError
and how they may occur. You also saw how to handle exceptions using the try-except
statement with or without else
or finally
. Next, you learned how to raise exceptions, chain them, and add more details to the exceptions. Lastly, you learned how to define your exception by subclassing the Exception
class.
Thank you for reading.
Top comments (0)