Python Try-Except: Handle Errors Without Crashing
Imagine a trapeze artist performing high above the ground. No matter how skilled they are, there's always a safety net below. If they slip, the net catches them and they can climb back up and try again.
Your Python code needs a safety net too. No matter how carefully you write a program, things can go wrong at runtime. A user might type "hello" when you expect a number. A file might not exist. A network connection might drop. Without error handling, your entire program crashes.
Python's try-except blocks are that safety net. They let you attempt something risky and catch the error if it happens, so your program keeps running gracefully instead of crashing in the user's face.
Why Do Python Programs Crash?
When Python hits a problem it can't solve on its own, it creates an exception — a special object that describes what went wrong. If nobody catches that exception, the program stops immediately and prints a traceback.
Here are a few common situations that cause exceptions:
Run each example above. You'll see that Python gives you the type of error (like ZeroDivisionError) and a message explaining the problem. These are useful clues, but in a real application, you don't want your users to see raw tracebacks.
How Does Basic try-except Work?
The try block wraps the code that might fail. The except block contains the code that runs only if an error occurs. If nothing goes wrong, the except block is skipped entirely.
Notice two important things. First, when the error happens on 10 / 0, Python immediately jumps to the except block — the print line after it is skipped. Second, the program continues running after the try-except block. No crash.
How to Catch Specific Exceptions
Instead of catching everything, you can tell Python exactly which type of error you want to handle. This is like a doctor diagnosing a specific illness rather than saying "you're sick."
When you run this and type something like "twenty", Python raises a ValueError. Our except block catches only that type. If a different error happened (like a TypeError), it would still crash — and that's a good thing, because it means you'll notice unexpected bugs.
You can also capture the error object using the as keyword. This gives you access to the error message:
What Is the else Block in try-except?
The else block runs only when the try block succeeds without any errors. It's the perfect place for code that should only execute when everything went well.
Why not just put that code inside the try block? Because code inside try might raise an exception you didn't intend to catch. The else block keeps your "success path" clean and separate from the risky code.
try:
value = int(user_input)
# This could raise a different
# error that gets caught by
# our ValueError handler
result = process(value)
except ValueError:
print("Bad input")try:
value = int(user_input)
except ValueError:
print("Bad input")
else:
# Only runs if int() worked
# Errors here won't be caught
result = process(value)What Is the finally Block?
The finally block runs no matter what — whether the code succeeded, failed, or even if a return statement was reached. It's guaranteed cleanup.
Think of it like washing your hands after cooking. Whether the recipe was a success or a disaster, you still wash your hands afterward.
Notice that "Division operation complete." prints in both cases — success and failure. The finally block even runs when there's a return inside try or except.
How to Catch Multiple Exceptions
A single try block might encounter different types of errors. You can handle each one differently with multiple except clauses:
Python checks each except clause from top to bottom and uses the first one that matches. Once a match is found, the rest are skipped.
If you want to handle two different exceptions the same way, you can group them in a tuple:
How to Raise Exceptions with raise
You aren't limited to catching errors — you can create and raise your own. The raise keyword tells Python to stop normal execution and create an exception.
This is how you enforce rules in your code. Instead of silently accepting bad data, you raise an exception that forces the caller to deal with the problem.
You can also re-raise an exception after doing some work. Just use raise with no arguments inside an except block:
The Full try-except-else-finally Pattern
All four blocks can work together in one structure. Here's the complete pattern and when each block runs:
Error Handling Best Practices
Good error handling makes your code robust without making it unreadable. Here are the key guidelines:
1. Catch specific exceptions. Never use a bare except: in production code. Be explicit about what errors you expect.
2. Keep try blocks small. Only wrap the line or lines that might actually fail. The more code you put in a try block, the harder it is to know what caused the error.
try:
data = load_file(path)
parsed = parse(data)
result = transform(parsed)
save(result)
except Exception:
print("Something failed")try:
data = load_file(path)
except FileNotFoundError:
print(f"File not found: {path}")
else:
parsed = parse(data)
result = transform(parsed)
save(result)3. Don't use exceptions for normal flow. If you can check a condition before it causes an error, do that instead. Exceptions are for exceptional situations.
try:
value = my_dict[key]
except KeyError:
value = defaultvalue = my_dict.get(key, default)4. Always provide useful error messages. When you raise or log an error, include enough context so someone reading the message can understand what happened.
Practice Exercises
Write a function called safe_divide(a, b) that returns the result of a / b. If b is zero, it should catch the ZeroDivisionError and return the string "Cannot divide by zero" instead.
Print the result of calling safe_divide(10, 2) and then safe_divide(10, 0).
Read the code below carefully and predict what it will print. Type the exact output.
try:
print("A")
x = int("hello")
print("B")
except ValueError:
print("C")
else:
print("D")
finally:
print("E")The function below is supposed to convert a list of strings to integers, skipping any values that can't be converted. But it has bugs! Fix the code so it produces the correct output.
Expected output: [1, 2, 3]
Write a function called validate_score(score) that:
TypeError with message "Score must be a number" if score is not an int or floatValueError with message "Score must be between 0 and 100" if score is less than 0 or greater than 100"Valid score: X" where X is the score, if the input is validThen test it by calling the function with 85, "high", and 150, catching each error and printing the error message.
Write a function called safe_lookup(data, key, index) that:
1. Looks up key in the dictionary data (which should return a list)
2. Then returns the item at position index in that list
3. Handles these errors:
- KeyError: print "Key 'X' not found" (where X is the key)
- IndexError: print "Index X out of range" (where X is the index)
- TypeError: print "Invalid operation"
Return None for any error case.
Test with these three calls:
safe_lookup({"grades": [90, 85, 92]}, "grades", 1) (should print 85)safe_lookup({"grades": [90, 85, 92]}, "scores", 0) (KeyError)safe_lookup({"grades": [90, 85, 92]}, "grades", 10) (IndexError)Write a function called calculate(expression) that takes a string like "10 + 5" and returns the result. It should:
1. Split the expression into parts (number, operator, number)
2. Support +, -, *, / operators
3. Handle these errors:
- ValueError if the numbers can't be converted: print "Invalid numbers in expression"
- ZeroDivisionError for division by zero: print "Cannot divide by zero"
- For any unknown operator, raise a ValueError with message "Unknown operator: X"
4. Use else to print "Result: X" on success
5. Use finally to print "Calculation complete"
Test with: "10 + 5", "20 / 0", and "abc + 2"