Python Debugging Handbook
Bugs and errors are the most common problems in programming. In this lesson we will learn how to debug and handle errors in Python.
5.3. Bugs
Bugs are errors in the code that make our code inoperable. Some of them are simple, some of them are hard to find and some of them are hard to fix. There are bugs that are immediately spotted by the interpreter, whilst other bugs are more difficult to find and fix because they are caused by the faulty logic of the program, or by some other external reason.
Let us see the most common types of bugs/errors in Python.
5.3.1. Common Error Messages
Python interpreter will indicate these errors to you in the form of an error message. These error messages are very helpful for debugging and fixing bugs.
5.3.1.1. SyntaxError - invalid syntax
Syntax errors are errors in the code that you write that are caused by a syntax mistake. For example, if you write a forbidden keyword as a variable name or if you forget to close a bracket, etc. Syntax errors can be easy to find and fix. The interpreter will indicate the line where the syntax error occurred.
syntax error -
1name = ‘John’
1
2
3
| # syntax error - missing colon
if 5 > 2
print("Five is greater than two!")
|
1
2
3
4
| Cell In[4], line 2
if 5 > 2
^
SyntaxError: expected ':'
|
5.3.1.2. IndentationError - unexpected indent
- IndentationError is another type of error in Python. An indentation error happens when you forget to use the correct indentation.
1
2
3
| # indentation error
if 5 > 2:
print("Five is greater than two!")
|
1
2
3
4
| Cell In[5], line 3
print("Five is greater than two!")
^
IndentationError: expected an indented block after 'if' statement on line 2
|
1
2
3
| for i in range(10):
if i == 5:
break
|
1
2
3
4
| File <string>:3
break
^
IndentationError: unindent does not match any outer indentation level
|
### 5.3.1.3. NameError - name ‘variable’ is not defined
- A name error happens when you try to use a variable or function that has not been defined yet, or it has been deleted.
1
2
3
4
| # NameError: name 'x' is not defined
y = 20
print(x + y)
|
1
2
3
4
5
6
7
8
9
10
11
| ---------------------------------------------------------------------------
NameError Traceback (most recent call last)
Cell In[12], line 3
1 # NameError: name 'x' is not defined
2 y = 20
----> 3 print(x + y)
NameError: name 'x' is not defined
|
1
2
3
4
5
6
7
| # Name error - variable deleted
name = 'Tom'
print(name)
del name
print(name)
|
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
| Tom
---------------------------------------------------------------------------
NameError Traceback (most recent call last)
Cell In[13], line 6
4 print(name)
5 del name
----> 6 print(name)
NameError: name 'name' is not defined
|
5.3.1.4. AttributeError - ‘object’ has no attribute ‘name’
- An AttributeError is a type of error that happens when you try to access an attribute or method that does not exist. For example, if you try to access the length of a string, you will get an error.
1
2
3
4
5
| # Attribute error example
my_tuple = (1, 2, 3)
my_tuple.append(4) # AttributeError: 'tuple' object has no attribute 'append'
|
1
2
3
4
5
6
7
8
9
10
11
| ---------------------------------------------------------------------------
AttributeError Traceback (most recent call last)
Cell In[15], line 4
1 # Attribute error example
3 my_tuple = (1, 2, 3)
----> 4 my_tuple.append(4) # AttributeError: 'tuple' object has no attribute 'append'
AttributeError: 'tuple' object has no attribute 'append'
|
5.3.1.5. FileNotFoundError - [Errno 2] No such file or directory: ‘file’
- A FileNotFoundError is a type of error that happens when you try to open a file that does not exist. For example, if you try to open a file that does not exist, you will get an error.
1
2
3
4
5
6
| # File not found error example
my_file = open('my_file.txt', 'r')
my_file.read() # FileNotFoundError: [Errno 2] No such file or directory: 'my_file.txt'
my_file.close()
|
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
| ---------------------------------------------------------------------------
FileNotFoundError Traceback (most recent call last)
Cell In[17], line 3
1 # File not found error example
----> 3 my_file = open('my_file.txt', 'r')
4 my_file.read() # FileNotFoundError: [Errno 2] No such file or directory: 'my_file.txt'
6 my_file.close()
File ~/anaconda3/envs/python312/lib/python3.12/site-packages/IPython/core/interactiveshell.py:324, in _modified_open(file, *args, **kwargs)
317 if file in {0, 1, 2}:
318 raise ValueError(
319 f"IPython won't let you open fd={file} by default "
320 "as it is likely to crash IPython. If you know what you are doing, "
321 "you can use builtins' open."
322 )
--> 324 return io_open(file, *args, **kwargs)
FileNotFoundError: [Errno 2] No such file or directory: 'my_file.txt'
|
5.3.1.6. IndexError - list index out of range
- An IndexError is a type of error that happens when you try to access an element in a list or tuple that is out of range.
1
2
3
4
| # IndexError
my_list = [1, 2, 3]
my_list[3] # IndexError: list index out of range
|
1
2
3
4
5
6
7
8
9
10
11
| ---------------------------------------------------------------------------
IndexError Traceback (most recent call last)
Cell In[28], line 4
1 # IndexError
3 my_list = [1, 2, 3]
----> 4 my_list[3] # IndexError: list index out of range
IndexError: list index out of range
|
5.3.1.7. ImportError - No module named ‘module’
- An ImportError is a type of error that happens when you try to import a module that does not exist.
1
2
3
4
| # ImportError
import superman # ModuleNotFoundError: No module named 'superman'
supeman.fly() # AttributeError: 'module' object has no attribute 'fly'
|
1
2
3
4
5
6
7
8
9
10
11
| ---------------------------------------------------------------------------
ModuleNotFoundError Traceback (most recent call last)
Cell In[18], line 2
1 # ImportError
----> 2 import superman # ModuleNotFoundError: No module named 'superman'
4 supeman.fly() # AttributeError: 'module' object has no attribute 'fly'
ModuleNotFoundError: No module named 'superman'
|
5.3.1.8. TypeError
- A TypeError is a type of error that happens when you try to perform an operation that is not possible.
- when you want to use + operator on a string and an integer
- when you give a wrong number of arguments to a function
- when you try to call an object as if it were a function, but it’s not callable.
1
2
3
4
5
| # TypeError string and number
x = 5
y = "Hello"
print(x + y) # TypeError: unsupported operand type(s) for +: 'int' and 'str'
|
1
2
3
4
5
6
7
8
9
10
11
| ---------------------------------------------------------------------------
TypeError Traceback (most recent call last)
Cell In[19], line 5
3 x = 5
4 y = "Hello"
----> 5 print(x + y) # TypeError: unsupported operand type(s) for +: 'int' and 'str'
TypeError: unsupported operand type(s) for +: 'int' and 'str'
|
1
2
3
4
5
6
| # TypeError - wrong number of arguments in a function
def add(a, b):
return a + b
add(1, 2, 3)
|
1
2
3
4
5
6
7
8
9
10
11
| ---------------------------------------------------------------------------
TypeError Traceback (most recent call last)
Cell In[20], line 6
2 def add(a, b):
3 return a + b
----> 6 add(1, 2, 3)
TypeError: add() takes 2 positional arguments but 3 were given
|
1
2
3
4
5
6
7
| # TypeError: call non callable object
my_list = [1, 2, 3]
my_list() # my_list is a list, not a function
|
1
2
3
4
5
6
7
8
9
10
11
| ---------------------------------------------------------------------------
TypeError Traceback (most recent call last)
Cell In[23], line 4
1 # TypeError: call non callable object
3 my_list = [1, 2, 3]
----> 4 my_list() # my_list is a list, not a function
TypeError: 'list' object is not callable
|
5.3.1.9. ValueError
- A ValueError is a type of error that happens when you try to convert a value to a different type.
- it can occur when you give a wrong value to a function.
- it can also occur when you try to use mutable type as a key in a dictionary.
1
2
3
4
| # ValueError - convert non numeric string to int
my_string = "Hello"
int(my_string) # ValueError: invalid literal for int() with base 10: 'Hello'
|
1
2
3
4
5
6
7
8
9
10
11
| ---------------------------------------------------------------------------
ValueError Traceback (most recent call last)
Cell In[24], line 4
1 # ValueError - convert non numeric string to int
3 my_string = "Hello"
----> 4 int(my_string) # ValueError: invalid literal for int() with base 10: 'Hello'
ValueError: invalid literal for int() with base 10: 'Hello'
|
1
2
3
4
5
6
7
| # call a function with a wrong type of argument
def divide(a, b):
return a / b
divide('Hello', 'World') # ValueError: unsupported operand type(s) for +: 'str' and 'str'
# TypeError: add_two_numbers() takes 2 positional arguments but 3 were given
|
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
| ---------------------------------------------------------------------------
TypeError Traceback (most recent call last)
Cell In[27], line 6
2 def divide(a, b):
3 return a / b
----> 6 divide('Hello', 'World') # ValueError: unsupported operand type(s) for +: 'str' and 'str'
7 # TypeError: add_two_numbers() takes 2 positional arguments but 3 were given
Cell In[27], line 3, in divide(a, b)
2 def divide(a, b):
----> 3 return a / b
TypeError: unsupported operand type(s) for /: 'str' and 'str'
|
5.4. Debugging and Error Handling
5.4.1. SyntaxError
- Syntax Errors block the interpreter from executing your code. They need to be fixed before the code can be executed.
5.4.2. Exceptions (Errors)
- An exception is an error that occurs during the execution of a program. It interrupts the normal flow of the program and allows you to handle the error gracefully.
- When an exception occurs, Python will normally stop and generate an error message.
These exceptions can be handled using the try
statement:
- syntax:
1
2
3
4
5
6
7
8
| try:
# code that may raise an exception
except:
# code to handle the exception
else:
# code that will execute if there is no exception
finally:
# code that will always execute
|
try
- tries to execute the code in the try
block.except
- If an exception occurs, the code in the except
block will be executed.- if no error type is specified, it will catch any exception - not a good practice
else
- If no exception occurs, the code in the else
block will be executed.finally
- If an exception occurs, the code in the finally
block will be executed.
1
2
3
4
5
6
7
8
9
10
| # try except example
num = input("Enter a number: ")
try:
result = 10 / num
except: # catch all errors - we don't know what went wrong
print("There's an error")
else:
print("The result is", result)
|
1
2
3
4
5
6
7
8
9
10
11
12
| # try except example - with error specification
try:
x = 5
y = 0
z = x / y
except ZeroDivisionError: # ZeroDivisionError: division by zero error
print("Error: Cannot divide by zero.")
else:
print("The result is", z)
finally:
print("End of program.")
|
1
2
| Error: Cannot divide by zero.
End of program.
|
5.4.3. Multiple Exceptions
- Sometimes we have a situation in which our code can produce multiple errors in a single try block. There are different ways to handle multiple exceptions, but they are not evenly effective.
Let us see an example:
1
2
3
4
5
6
7
8
| num1 = input('Number 1: ')
num2 = input('Number 2: ')
try:
num1 = int(num1) # possible ValueError
num2 = int(num2) # possible ValueError
print(num1/num2) # possible ZeroDivisionError
except:
print('Error: Invalid input.')
|
Yes, but what input was invalid? Which error did we get?
We can try to catch the errors using the except
block.
1
2
3
4
5
6
7
8
| num1 = input('Number 1: ')
num2 = input('Number 2: ')
try:
num1 = int(num1) # possible ValueError
num2 = int(num2) # possible ValueError
print(num1/num2) # possible ZeroDivisionError
except Exception as e:
print(e)
|
This is a bit better, but we want to do something if the error occurs. We can use the if else
block.
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
| import sys
num1 = input('Number 1: ')
num2 = input('Number 2: ')
try:
num1 = int(num1)
num2 = int(num2)
print(num1/num2)
except Exception as e:
if 'zero' in str(e):
print('Division by 0')
sys.exit()
elif 'base 10' in str(e):
print('Not a number')
sys.exit()
|
That solves one problem, but introduces others. Try to understand why.
- Multiple errors can be handled by changing the order of the
except
blocks. Our first code could be be rewriten as follows:
1
2
3
4
5
6
7
8
9
10
11
12
13
| while True:
num1 = input('Number 1: ')
num2 = input('Number 2: ')
try:
num1 = int(num1) # possible ValueError
num2 = int(num2) # possible ValueError
print(num1/num2) # possible ZeroDivisionError
except ValueError:
print('Error: Please, insert valid numbers.')
except ZeroDivisionError:
print('Error: Cannot divide by zero. Please, insert a valid number.')
else:
break
|
1
2
| Error: Cannot divide by zero. Please, insert a valid number.
1.0
|
This is another story! 😄
5.4.4. Custom Errors
-Real Python: Python’s Raise: Effectively Raise Errors in Your Code
- We can define our own errors using the
raise
statement.
1
2
3
4
5
6
| # invalid input value
name = 'John Wayne'
if name == 'John Wayne':
raise ValueError('Please, enter a valid name.')
|
1
2
3
4
5
6
7
8
9
10
11
| ---------------------------------------------------------------------------
ValueError Traceback (most recent call last)
Cell In[1], line 6
3 name = 'John Wayne'
5 if name == 'John Wayne':
----> 6 raise ValueError('Please, enter a valid name.')
ValueError: Please, enter a valid name.
|
Choosing the Right Exception Type
Python has many built-in exceptions. You can raise different types of exceptions depending on the error condition. Here are some commonly used ones:
TypeError
: Raised when an operation or function is applied to an object of inappropriate type.ValueError
: Raised when a function receives an argument of the correct type but an inappropriate value.IndexError
: Raised when you try to access an index that is out of range.
Examples of Raising Specific Exceptions
Example 1: TypeError
Raise a TypeError
if the input is not an integer:
1
2
3
4
| x = "hello"
if not isinstance(x, int):
raise TypeError("Only integers are allowed")
|
Here, the isinstance
function checks if x
is an integer. If it’s not, a TypeError
is raised with the message “Only integers are allowed”.
Example 2: ValueError
Raise a ValueError
if the input is not within an expected range:
1
2
3
4
| age = -5
if age < 0 or age > 120:
raise ValueError("Age must be between 0 and 120")
|
This code checks if age
is within the range of 0 to 120. If it’s not, a ValueError
is raised with the message “Age must be between 0 and 120”.
Example 3: IndexError
Raise an IndexError
if trying to access an index that is out of range:
1
2
3
4
5
| my_list = [1, 2, 3]
index = 5
if index >= len(my_list):
raise IndexError("Index out of range")
|
This example checks if index
is within the bounds of my_list
. If index
is out of range, an IndexError
is raised with the message “Index out of range”.
Custom Exceptions
You can also define your own exceptions by creating a new class that inherits from the Exception
class:
1
2
3
4
5
6
7
8
| class CustomError(Exception):
pass
def check_condition(x):
if x < 0:
raise CustomError("Custom error: x cannot be negative")
check_condition(-1)
|
In this example, CustomError
is a user-defined exception. The function check_condition
raises this exception if x
is less than 0.
Summary
Raising exceptions in Python allows you to handle error conditions gracefully. You can use built-in exception types or define your own to provide meaningful error messages and keep your code robust. Here are some key points:
- Use the
raise
keyword to raise an exception. - Choose the appropriate exception type based on the error condition.
- Provide a clear error message to help understand the issue.
- Consider defining custom exceptions for specific use cases.
By understanding and using exceptions effectively, you can make your Python programs more reliable and easier to debug.