try / except / finally
try / except / finally
Errors are inevitable. Python's exception-handling mechanism lets you respond to errors gracefully instead of crashing.
The Full Syntax
try:
# code that might raise an exception
except SomeError as e:
# handle a specific error
except (TypeError, ValueError) as e:
# handle multiple exception types
except Exception as e:
# catch-all for any standard exception
else:
# runs only if NO exception was raised in try
finally:
# ALWAYS runs — cleanup code goes hereThe else Clause
The else block runs only when the try block completes without an exception. It is the right place for code that should only execute on success but shouldn't be inside try itself (to avoid accidentally masking exceptions):
try:
result = int(user_input)
except ValueError:
print("Not a valid number")
else:
print(f"You entered: {result}") # only if no ValueErrorThe finally Clause
finally always executes, whether or not an exception occurred. Use it to release resources:
f = open("data.txt")
try:
data = f.read()
finally:
f.close() # guaranteed to run(In practice, use a with statement instead — covered in the next lesson.)
Raising Exceptions
raise ValueError("Age cannot be negative")
# Re-raise inside an except block
try:
risky()
except ValueError as e:
log(e)
raise # re-raise the same exceptionException Hierarchy
All built-in exceptions inherit from BaseException. The most important branches:
BaseException
├── SystemExit
├── KeyboardInterrupt
└── Exception
├── ArithmeticError
│ ├── ZeroDivisionError
│ └── OverflowError
├── LookupError
│ ├── IndexError
│ └── KeyError
├── TypeError
├── ValueError
├── OSError
│ └── FileNotFoundError
└── RuntimeErrorCatch more specific exceptions first (put them above broader ones) to avoid masking useful error detail.
Best Practices
- Catch the most specific exception type you expect.
- Never use a bare
except:— it catchesSystemExitandKeyboardInterrupttoo. - Keep the
tryblock as short as possible. - Use
finally(or a context manager) to guarantee resource cleanup.
Code Examples
def safe_divide(a, b):
try:
result = a / b
except ZeroDivisionError:
print("Error: cannot divide by zero")
return None
except TypeError as e:
print(f"Error: invalid types — {e}")
return None
else:
print(f"Success: {a} / {b} = {result:.4f}")
return result
finally:
print("safe_divide() finished")
print("--- Test 1 ---")
safe_divide(10, 4)
print("--- Test 2 ---")
safe_divide(10, 0)
print("--- Test 3 ---")
safe_divide(10, "two")The else block runs only when no exception occurs in try. finally always runs — even when an exception is caught. This ordering ensures resources are always released and success-only logic is cleanly separated from error handling.
data = {"name": "Alice", "score": "95"}
def process(d, key):
try:
value = d[key] # may raise KeyError
number = int(value) # may raise ValueError
result = 100 / number # may raise ZeroDivisionError
return result
except KeyError:
print(f"Key '{key}' not found in data")
except ValueError:
print(f"Cannot convert '{d.get(key)}' to int")
except ZeroDivisionError:
print("Score is zero — division undefined")
except Exception as e:
print(f"Unexpected error: {type(e).__name__}: {e}")
process(data, "score")
process(data, "age")
process({"score": "abc"}, "score")Listing specific exceptions before broad ones ensures correct handling. type(e).__name__ gives the exception class name without importing anything extra. The except Exception catch-all at the end handles anything unforeseen.
def validate_age(age):
if not isinstance(age, int):
raise TypeError(f"Age must be an int, got {type(age).__name__}")
if age < 0:
raise ValueError(f"Age cannot be negative, got {age}")
if age > 150:
raise ValueError(f"Age {age} seems unrealistic")
return age
def create_user(name, age):
try:
valid_age = validate_age(age)
return {"name": name, "age": valid_age}
except (TypeError, ValueError) as e:
print(f"Validation failed: {e}")
raise # re-raise for the caller to handle
# Valid
user = create_user("Alice", 30)
print("Created:", user)
# Invalid — caught and re-raised
try:
create_user("Bob", -5)
except ValueError as e:
print("Caught in outer scope:", e)raise (bare) inside an except block re-raises the current exception with its original traceback intact — better than raise e, which resets the traceback. This pattern lets intermediate layers log errors while still propagating them.
Quick Quiz
1. When does the else clause of a try/except block execute?
2. What is the risk of using a bare except: clause (with no exception type)?
3. What does a bare raise statement (without arguments) do inside an except block?
Was this lesson helpful?