The four clauses
else holds the code that should run only on success, and keeping it out of the try means you are not accidentally catching exceptions from it. finally runs whatever happens, which is what makes it the right place for cleanup — though a with block is usually better still.
try:
data = json.loads(text)
except json.JSONDecodeError as err:
log(f"bad payload: {err}")
data = {}
else:
log("parsed cleanly") # only if try raised nothing
finally:
close(handle) # always — return, raise, break, everything
Catch narrowly
A bare except: catches KeyboardInterrupt and SystemExit, so it can stop a program being stopped. except Exception: is the widest you should ever write, and only where you genuinely intend to keep going.
The real rule: catch the exception you know how to handle, at the level where you know what to do about it. An exception you cannot act on should travel up to somewhere that can.
except Exception: # too wide, but at least it lets KeyboardInterrupt through
except (ValueError, TypeError) as err: # a tuple, when several are equivalent
except: # NEVER — this catches SystemExit and Ctrl-C too
The hierarchy, briefly
LookupError covers KeyError and IndexError; ArithmeticError covers ZeroDivisionError and OverflowError. Catching the parent is how you say "any of that family".
| Exception | Means |
|---|---|
ValueError | The right type, the wrong value — int("abc") |
TypeError | The wrong type — "3" + 4 |
KeyError / IndexError | Not in the dict / past the end of the sequence |
AttributeError | No such attribute — often None where an object was expected |
FileNotFoundError | A subclass of OSError, like most I/O problems |
StopIteration | An iterator is finished — the protocol, not a failure |
Raising well
A good message says what was expected, what arrived, and — if you can — what to do. Define your own exception class when callers might want to catch your failure specifically; inherit from Exception, never from BaseException.
raise ... from err preserves the chain, so the traceback shows both. A bare raise inside an except re-raises the current exception untouched, which is how you log and then let it continue.
if quantity < 0:
raise ValueError(f"quantity must be at least 0, got {quantity}")
class CartEmpty(Exception):
"""Raised when checkout is attempted with nothing in the cart."""
try:
fetch(url)
except TimeoutError as err:
raise ServiceUnavailable(url) from err # keeps the original in __cause__
Ask forgiveness, not permission
EAFP is the Python house style, and not only for taste: the check-then-act version does the work twice and can be wrong by the time it acts. try costs almost nothing when nothing is raised.
The exception is a loop where failure is the common case — raising is not free, so a dict.get beats an exception you expect to hit a million times.
# LBYL — look before you leap: two lookups, and a race in between
if key in data:
use(data[key])
# EAFP — easier to ask forgiveness: one lookup, and no race
try:
use(data[key])
except KeyError:
default()