Your program has been running perfectly for ten minutes.
Then someone passes in a string where you expected a number. Or the file you're trying to read got deleted. Or the internet went down mid-request. Or a user typed nothing when your code expected input.
Your program crashes. A wall of red text appears. Everything stops.
This is not bad luck. This is normal. Errors happen in every real program. The question is not how to avoid them entirely. The question is how to catch them before they bring everything down.
What Actually Happens When Code Breaks
When Python hits a problem it cannot resolve, it raises an exception. If nothing catches that exception, the program stops and prints a traceback, that red wall of text.
numbers = [1, 2, 3]
print(numbers[10])
Traceback (most recent call last):
File "main.py", line 2, in <module>
print(numbers[10])
IndexError: list index out of range
Three pieces of information here. The file and line where it broke. The exact code that caused it. The type of error and what went wrong.
Read tracebacks from the bottom up. The last line is the actual error. Everything above it is the path Python took to get there.
Try and Except
Wrap risky code in a try block. Tell Python what to do if something goes wrong in the except block.
numbers = [1, 2, 3]
try:
print(numbers[10])
except IndexError:
print("That index doesn't exist in the list.")
Output:
That index doesn't exist in the list.
Program keeps running. No crash. No red text.
Python tries the code inside try. If an IndexError happens, it jumps to except IndexError and runs that block instead. If no error happens, the except block is skipped entirely.
Catching Different Error Types
Different problems raise different error types. You can handle each one differently.
def divide(a, b):
try:
result = a / b
return result
except ZeroDivisionError:
print("Cannot divide by zero.")
return None
except TypeError:
print("Both inputs must be numbers.")
return None
print(divide(10, 2))
print(divide(10, 0))
print(divide(10, "five"))
Output:
5.0
Cannot divide by zero.
Both inputs must be numbers.
Each except catches a specific error type. ZeroDivisionError when someone divides by zero. TypeError when the wrong type gets passed in. Both get handled cleanly without crashing.
The Errors You'll See Most Often
# NameError: used a variable that doesn't exist
print(username) # username was never defined
# TypeError: wrong type for the operation
result = "hello" + 5
# ValueError: right type, wrong value
number = int("hello") # "hello" can't become an integer
# IndexError: index out of range
items = [1, 2, 3]
print(items[99])
# KeyError: key doesn't exist in dictionary
data = {"name": "Alex"}
print(data["age"])
# FileNotFoundError: file doesn't exist
open("missing.txt", "r")
# ZeroDivisionError: dividing by zero
result = 10 / 0
These seven cover probably 80% of the errors you'll encounter in the next few months. When you see one, you now know the category of problem.
Getting the Error Message
Sometimes you want to know exactly what went wrong, not just that something went wrong.
try:
number = int("not a number")
except ValueError as e:
print(f"Something went wrong: {e}")
Output:
Something went wrong: invalid literal for int() with base 10: 'not a number'
as e captures the error object. e contains the full error message. Useful for logging problems or showing the user something specific.
Else and Finally
Two optional additions to try/except that come up in real code.
else runs only if no error happened.
try:
number = int("42")
except ValueError:
print("That's not a valid number.")
else:
print(f"Successfully converted: {number}")
Output:
Successfully converted: 42
finally runs no matter what. Error or no error.
try:
file = open("data.txt", "r")
content = file.read()
except FileNotFoundError:
print("File not found.")
finally:
print("This always runs.")
finally is useful for cleanup code that needs to run regardless of what happened. Closing database connections. Releasing locks. Logging that an operation completed. Even if an exception occurred and was not caught, finally still runs before Python propagates the error.
Raising Your Own Errors
You can raise errors intentionally. Useful when someone uses your function in a way that doesn't make sense.
def set_age(age):
if age < 0:
raise ValueError("Age cannot be negative.")
if age > 150:
raise ValueError("That age is not realistic.")
return age
try:
set_age(-5)
except ValueError as e:
print(f"Invalid input: {e}")
Output:
Invalid input: Age cannot be negative.
raise ValueError("message") creates and throws an error. The caller's try/except catches it. Your function refuses to work with bad data and tells you exactly why.
This is how professional code is written. Functions validate their inputs and raise clear errors when something is wrong. Far better than silently producing wrong answers.
A Real Pattern: Retrying on Failure
This shows up in data pipelines and API calls constantly.
def load_number(value):
try:
return int(value)
except ValueError:
return None
raw_data = ["42", "hello", "99", "abc", "17", "?"]
clean_numbers = []
for item in raw_data:
result = load_number(item)
if result is not None:
clean_numbers.append(result)
else:
print(f"Skipped invalid value: {item}")
print(f"Clean numbers: {clean_numbers}")
Output:
Skipped invalid value: hello
Skipped invalid value: abc
Skipped invalid value: ?
Clean numbers: [42, 99, 17]
Real data is messy. Some values won't convert. Some files won't exist. Some network calls will fail. Handle the failures, keep the successes, move on.
The One Mistake to Avoid
Catching everything silently.
try:
some_complicated_code()
except:
pass
This catches every possible error and does nothing about it. Your code fails silently. You have no idea what went wrong or where. Bugs become invisible. Do not do this.
At minimum, print the error. Better, log it. Best, handle specific error types and let unexpected ones crash loudly so you know they exist.
try:
some_complicated_code()
except Exception as e:
print(f"Unexpected error: {e}")
That bare except: with no type and no message is a trap. Avoid it.
Try This
Create errors_practice.py.
Write a function called safe_divide that takes two numbers and returns the result of dividing the first by the second. Handle the case where the second number is zero. Handle the case where either input is not a number. Return None for invalid inputs and print a clear message explaining what went wrong.
Then write a function called load_profile that takes a filename, opens it, reads the JSON inside using json.load, and returns the data as a dictionary. Handle the case where the file doesn't exist. Handle the case where the file exists but is not valid JSON. Return None for failures.
Test both functions with inputs that work and inputs that should fail. Make sure your program never crashes, just handles problems cleanly and keeps going.
What's Next
Your code now handles failure gracefully. Next is modules and packages, how Python lets you use code that other people wrote, which is how you'll eventually use NumPy, Pandas, PyTorch, and every other library in this series.
Top comments (0)