Error Handling
Learn how to handle errors and exceptions gracefully so your programs do not crash unexpectedly.
Two kinds of errors
When Python runs your program and something goes wrong, it raises an exception. If you do not handle it, your program crashes and shows an error message. There are two kinds of errors:
- Syntax errors: mistakes in how you wrote the code. Python catches these before the program even runs. Example: forgetting a colon at the end of an
ifstatement. - Runtime exceptions: errors that happen while the program is running. These can often be handled gracefully. Example: dividing by zero, or opening a file that does not exist.
# Syntax error: caught before anything runs
if age > 18
print("Adult")
# Runtime exception: the code is valid, but this moment fails
print("Start")
print(10 / 0) # ZeroDivisionError
print("End") # never reached
Notice that in the second example Start is printed, and then the program stops. End never appears. You cannot fix a syntax error by handling it, you must correct the code. But runtime exceptions you can plan for, and that is what the rest of this lesson is about.
Meet the common exceptions
Each exception has a name that tells you what went wrong. These are the ones you will see most:
int("hello") # ValueError: invalid literal for int()
10 / 0 # ZeroDivisionError: division by zero
name[10] # IndexError: list index out of range
person["email"] # KeyError: 'email'
open("missing.txt") # FileNotFoundError: No such file
int(None) # TypeError: int() argument must be a string
- ValueError: the type is right, but the value is not, like turning "hello" into a number
- ZeroDivisionError: dividing by zero
- IndexError: asking for a list position that is not there
- KeyError: asking for a dictionary key that is not there
- FileNotFoundError: opening a file that does not exist
- TypeError: using the wrong type, like adding text to a number
- NameError: using a variable that was never created
Learn to read the last line of an error message. It has the exception name and a short reason. That is nearly always enough to know what to fix.
try and except
To handle an exception, wrap the code that might fail in a try block. Write the handling code in an except block:
try:
number = int(input("Enter a number: "))
print(f"You entered: {number}")
except ValueError:
print("That is not a valid number.")
If the user types hello instead of a number, Python jumps to the except block and runs it instead of crashing. If there is no error, the except block is skipped completely.
The flow looks like this:
- Python runs the lines in
try, one by one - If a line raises a matching exception, Python stops right there and jumps to
except - Lines after the failing one inside
tryare skipped - The program continues normally after the whole block
try:
print("A")
x = int("oops") # fails here
print("B") # skipped
except ValueError:
print("C")
print("D")
A
C
D
Keep the try block small. Put only the risky lines in it, so you know which line caused the problem.
Handling several exceptions
One piece of code can fail in different ways, and you can react to each one differently. Write several except blocks:
try:
number = int(input("Enter a number: "))
result = 100 / number
print(f"100 divided by {number} is {result}")
except ValueError:
print("Please enter a valid number.")
except ZeroDivisionError:
print("You cannot divide by zero.")
Python checks them from top to bottom and runs the first one that matches. If the user types hello, the first block runs. If they type 0, the second one does. You can also catch two types with the same message by putting them in a tuple:
try:
value = int(input("Number: "))
print(10 / value)
except (ValueError, ZeroDivisionError):
print("Please give a number that is not zero.")
To see the original error text, save it with as:
try:
int("hello")
except ValueError as e:
print(f"Something went wrong: {e}")
Something went wrong: invalid literal for int() with base 10: 'hello'
else and finally
Two more optional blocks make try more powerful. The else block runs only if no exception occurred. The finally block always runs, whether an error happened or not:
try:
with open("data.txt", "r") as file:
content = file.read()
except FileNotFoundError:
print("File not found.")
else:
print("File read successfully.")
print(content)
finally:
print("This always runs.")
Why use else instead of putting those lines inside try? It keeps the risky code small and makes it clear that the success steps are separate. Why finally? It is meant for cleanup that must happen no matter what, like closing a connection or printing a final message.
try: the code that might failexcept: what to do if it failselse: what to do if it did not failfinally: what to do in every case
Raising your own exceptions
You can raise exceptions yourself when your code detects a problem. Use the raise keyword with an exception type and a message:
def set_age(age):
if age < 0 or age > 150:
raise ValueError(f"Age {age} is not a valid age.")
return age
try:
set_age(-5)
except ValueError as e:
print(f"Error: {e}")
# Output: Error: Age -5 is not a valid age.
Why would you do this? A function that receives a bad value should refuse to continue, rather than quietly produce a wrong answer. Raising an error loudly is often better than a silent mistake that shows up later. The code that called the function then decides how to deal with it.
Pick the exception type that fits. ValueError is right for a wrong value, and TypeError for a wrong type. Always add a clear message that explains the problem for the person who sees it.
A reusable input function
Here is a function that asks for a number and keeps asking until the user gives a valid one. It combines a loop, try and except, return and default parameters:
def get_integer(prompt, min_val=None, max_val=None):
while True:
try:
value = int(input(prompt))
if min_val is not None and value < min_val:
print(f"Please enter a number at least {min_val}.")
continue
if max_val is not None and value > max_val:
print(f"Please enter a number at most {max_val}.")
continue
return value
except ValueError:
print("That is not a valid number. Please try again.")
age = get_integer("Enter your age: ", 1, 120)
print(f"Your age is {age}.")
The loop only ends when return runs, and return only runs when the input is valid. Now you can use get_integer anywhere in your program instead of writing the same checks again and again.
except: that catches every possible error. This can hide real bugs and make them very hard to find later.Reading a traceback
When an exception is not handled, Python prints a traceback. It looks long, but it is very organized. Here is an example:
Traceback (most recent call last):
File "app.py", line 8, in <module>
show_average([])
File "app.py", line 4, in show_average
avg = total / len(numbers)
ZeroDivisionError: division by zero
Read it from the bottom up:
- The last line names the error and the reason:
ZeroDivisionError: division by zero - The line above it shows the exact code that failed:
avg = total / len(numbers) - The lines above that show how the program got there, from the first call down to the failure
Here the list was empty, so len(numbers) was 0. The fix is to check for that case, or catch the exception. Once you can read tracebacks calmly, debugging becomes much less stressful.
def show_average(numbers):
if not numbers:
print("No numbers to average.")
return
print(sum(numbers) / len(numbers))
Debugging habits
Handling errors is only half the story. You also need to find the bugs in your own code. These simple habits solve most problems:
- Read the error message. The exception name and line number point you straight to the problem.
- Print values. Put
print(variable)before the failing line and check that it holds what you expect. - Test small pieces. Run a function alone with a few inputs before using it in a bigger program.
- Change one thing at a time. If you change five things, you will not know which one fixed it.
- Take a break. A fresh look often shows an obvious typo.
def average(numbers):
print("DEBUG numbers:", numbers) # temporary check
return sum(numbers) / len(numbers)
Remove debug prints when you are done. Bugs are normal and every programmer deals with them daily. The skill is not avoiding mistakes, it is finding and fixing them calmly.
Key takeaways
- Syntax errors stop the program before it runs. Runtime exceptions happen while it runs
- Common ones:
ValueError,ZeroDivisionError,IndexError,KeyError,FileNotFoundError,TypeError tryholds risky code andexcepthandles the failure- Use several
exceptblocks, orexcept ... as eto see the message elseruns when nothing failed, andfinallyalways runsraiselets you signal your own problems- Catch only what you expect. Never hide errors with a bare
except: