try / except — Catching Errors Gracefully
Errors happen. Files don't exist, networks fail, users enter garbage data. Without error handling, these problems crash your program. Python's try/except lets you catch errors and respond gracefully instead of crashing.
Think of try/except like a safety net: you "try" something risky, and if it fails, the "except" block catches the error and handles it:
try:
number = int(input("Enter a number: "))
result = 10 / number
print(f"10 / {number} = {result}")
except ValueError:
print("That's not a valid number!")
except ZeroDivisionError:
print("Cannot divide by zero!")
except Exception as e:
print(f"Unexpected error: {e}")
Python checks each except clause from top to bottom and runs the first one that matches the error type. Always put more specific exceptions before general ones — if you put Exception first, it would catch everything and the specific handlers would never run.
Common exception types you'll encounter
ValueError (wrong value), TypeError (wrong type), KeyError (missing dict key), IndexError (list index out of range), FileNotFoundError, AttributeError (missing attribute), ImportError (missing module).
else and finally
The full try/except syntax includes two optional clauses: else runs only if no exception occurred, and finally runs always, whether or not an exception happened. This is perfect for cleanup:
try:
f = open("data.txt", "r")
except FileNotFoundError:
print("File not found!")
else:
# Only runs if open() succeeded
content = f.read()
print(f"Read {len(content)} chars")
f.close()
finally:
# Always runs — cleanup, logging, etc.
print("Operation complete")
else = "no errors happened", finally = "always runs". Use finally for cleanup that must happen regardless of success or failure.Raising Exceptions
You can raise your own exceptions with the raise keyword. This is how you signal that something has gone wrong in your code — bad input, violated business rules, impossible states:
def withdraw(amount, balance):
if amount <= 0:
raise ValueError("Amount must be positive")
if amount > balance:
raise ValueError(f"Insufficient funds: need {amount}, have {balance}")
return balance - amount
try:
new_balance = withdraw(-50, 100)
except ValueError as e:
print(f"Error: {e}")
try:
new_balance = withdraw(150, 100)
except ValueError as e:
print(f"Error: {e}")
Error: Amount must be positive Error: Insufficient funds: need 150, have 100
Custom Exceptions
For larger applications, create your own exception classes. This lets callers handle different error types differently. A common pattern is a base exception for your app, with specific subclasses:
class AppError(Exception):
"""Base exception for our application."""
pass
class NotFoundError(AppError):
def __init__(self, resource, resource_id):
self.resource = resource
self.id = resource_id
super().__init__(f"{resource} with id {resource_id} not found")
class ValidationError(AppError):
def __init__(self, field, message):
self.field = field
super().__init__(f"Validation error on '{field}': {message}")
# Usage
try:
raise NotFoundError("User", 42)
except NotFoundError as e:
print(f"Not found: {e}")
print(f"Resource: {e.resource}, ID: {e.id}")
except AppError as e:
print(f"App error: {e}")
Not found: User with id 42 not found Resource: User, ID: 42
EAFP vs LBYL
Python has two philosophies for handling potential errors:
LBYL (Look Before You Leap): Check if something will work before trying it.
EAFP (Easier to Ask Forgiveness than Permission): Just try it and handle the error if it fails.
Python strongly favors EAFP. Here's why:
# LBYL style — check first (less Pythonic)
if "key" in dictionary:
value = dictionary["key"]
else:
value = default_value
# EAFP style — try and catch (Pythonic)
try:
value = dictionary["key"]
except KeyError:
value = default_value
# Even better: use the built-in .get() method
value = dictionary.get("key", default_value)
EAFP is preferred because: (1) it avoids race conditions where the state changes between the check and the action, (2) it's often faster because the "happy path" (no error) runs without any checks, and (3) it's more readable for experienced Python developers.
⚠️ Common Mistake: Catching Too Broadly
Wrong:
try:
do_something()
except: # Bare except catches EVERYTHING!
pass # Silently swallows ALL errors
Why: Bare except catches SystemExit, KeyboardInterrupt, and every other exception — making debugging impossible. pass silently ignores the error so you never know something went wrong.
Instead:
try:
do_something()
except Exception as e:
logger.error(f"Failed: {e}")
# Or: raise — re-raise to let caller handle it
🔍 Deep Dive: Exception Chaining
Python 3 supports exception chaining with raise ... from .... When you catch one exception and raise another, you can preserve the original cause: raise AppError("processing failed") from original_error. The traceback will show both exceptions with a "The above exception was the direct cause of the following exception" message. This is invaluable for debugging complex systems.
Context Managers
The with statement ensures resources are properly cleaned up. You've seen it with files, but it works with any object that implements __enter__ and __exit__:
# Files (most common)
with open("file.txt") as f:
data = f.read()
# Database connections
# with connect("mydb") as conn:
# conn.execute("SELECT ...")
# Locks (threading)
# with lock:
# shared_resource.update()
# You can use multiple context managers
with open("input.txt") as src, open("output.txt", "w") as dst:
dst.write(src.read().upper())
except. Use EAFP style. Create custom exceptions for your application. Always use with for resource management.