Python's exception handling has four clauses: try, except, else, finally. Most code uses just try/except; the other two earn their keep in specific situations. else runs when try succeeded without raising. finally always runs, exception or not — used for cleanup that must happen.
Why a separate else clause
Putting code after the try body but inside the try block widens what except can catch — a bug there gets silently caught. Putting the same code in else means the except only catches what was actually risky, and the "everything went fine, here's the next step" logic is separate. Small clarification, real bug-prevention.
Catching multiple exception types
You can catch a tuple of types: except (ValueError, TypeError):. You can have multiple except clauses, checked in order — first match wins. You can also bind the exception object: except ValueError as e:.
Bare except is almost always wrong
except: with no type catches everything, including KeyboardInterrupt (Ctrl+C) and SystemExit. Code that should respond to those is silently swallowing them. Always prefer except Exception at the broadest, and ideally narrow further to the specific type you expect.
Principle: Catch the narrowest exception type that handles the failure mode you can recover from. Catching too broadly hides bugs you didn't expect; catching too narrowly leaves real failures unhandled. The right grain depends on the operation; default to specific types over Exception.
Code
try / except / else / finally — all four·python
def parse_int(s):
try:
result = int(s)
except ValueError:
print("could not parse")
return None
else:
print("parsed successfully")
return result
finally:
print("always runs (cleanup, logging, etc.)")
print(parse_int("42"))
# parsed successfully
# always runs (cleanup, logging, etc.)
# 42
print(parse_int("not a number"))
# could not parse
# always runs (cleanup, logging, etc.)
# None
Multiple except clauses — first match wins·python
def lookup(d, key):
try:
return int(d[key])
except KeyError:
return f"key {key!r} missing"
except ValueError:
return f"key {key!r} not an int"
except TypeError:
return f"d is not a dict"
print(lookup({"a": "42"}, "a")) # 42
print(lookup({"a": "42"}, "b")) # key 'b' missing
print(lookup({"a": "x"}, "a")) # key 'a' not an int
print(lookup(None, "a")) # d is not a dict
# Tuple of types — single except handles multiple
try:
pass
except (KeyError, ValueError) as e:
print(type(e).__name__, e)
Why else helps — narrowing what except sees·python
# WITHOUT else — risky
def bad(file_path):
try:
with open(file_path) as f:
data = f.read()
print("file size:", len(data)) # if this fails, except catches
value = int(data) # this too
except FileNotFoundError:
print("missing file")
except ValueError:
print("bad number")
# but a bug in print() or len() also gets caught — silently swallowed
# WITH else — except only catches the actual risky operations
def good(file_path):
try:
with open(file_path) as f:
data = f.read()
except FileNotFoundError:
print("missing file")
return
else:
print("file size:", len(data))
value = int(data) # if THIS fails, NOT caught
return value
Bare except — almost always wrong·python
# DON'T
try:
risky()
except: # bare except
pass # swallows EVERYTHING including Ctrl+C
# DO — Exception is the broadest reasonable
try:
risky()
except Exception as e:
print("got:", type(e).__name__, e)
# BETTER — narrow to what you actually expect
try:
risky()
except (ValueError, KeyError) as e:
print("expected failure:", e)
# Other exceptions propagate up — that's correct
Write a function safe_divide(a, b) that returns a / b, but: catches TypeError (returns the string "bad types"), catches ZeroDivisionError (returns float('inf') if a > 0, float('-inf') if a < 0, 0.0 if a == 0), uses else to print 'success' before returning, and finally to print 'attempted'. Test with at least four input combinations including normal, divide by zero, and wrong types.
Progress
Progress is local-only — sign in to sync across devices.