Python Exceptions Cheat Sheet: The Hierarchy and the Fixes
Read the last line of the traceback first: the exception name plus message is the diagnosis; the traceback above it is the crime scene.
The Hierarchy That Explains Catching
Exceptions are classes, and except matches by inheritance. BaseException → Exception → specific types. Everything below inherits from Exception, which is why except Exception catches nearly everything — and why you should not do it casually.
BaseException
├── SystemExit # sys.exit() — don't catch
├── KeyboardInterrupt # Ctrl+C — don't catch
└── Exception
├── ArithmeticError → ZeroDivisionError, OverflowError
├── LookupError → IndexError, KeyError
├── OSError → FileNotFoundError, PermissionError, FileExistsError
├── ValueError, TypeError, AttributeError
├── ImportError → ModuleNotFoundError
├── RuntimeError → RecursionError
└── StopIteration, NotImplementedErrorThe Exceptions You Actually Meet
| Exception | Triggered by | Fix |
|---|---|---|
| TypeError | Wrong type: "a" + 1, calling a non-callable | Convert explicitly; check the type before the operation |
| ValueError | Right type, bad value: int("abc") | Validate before parsing; use try/except around the conversion |
| KeyError | Missing dict key | d.get(k, default), or check k in d. KeyError guide |
| IndexError | Sequence index out of range | Check length, use slicing, or iterate directly |
| AttributeError | Attribute doesn't exist on the object | Check for None; verify the attribute name. AttributeError guide |
| ModuleNotFoundError | import of a missing module | Install it; check the venv. Guide |
| FileNotFoundError | Opening a path that doesn't exist | Check the path relative to CWD; use pathlib |
| PermissionError | OS denied the operation | Check file ownership/mode, not sudo first. Guide |
| ZeroDivisionError | x / 0, x % 0 | Guard the denominator; or catch and return a sentinel |
| IndentationError | Mixed tabs/spaces or wrong indent | Pick one indentation style; re-indent the block. Guide |
| RecursionError | Recursion beyond the limit | Add a base case, or convert to iteration |
| StopIteration | next() on an exhausted iterator | Use a for loop or provide a default to next() |
The except Ordering Trap
Python checks except clauses top to bottom and uses the first match — so a broad clause placed first shadows the specific ones below it:
# WRONG: ValueError is unreachable
try:
x = int(user_input)
except Exception:
print("generic") # always wins
except ValueError:
print("bad number") # dead codeThe same rule makes except (OSError, IOError) redundant (IOError is an alias of OSError). Order: specific first, broad last. Linters flag unreachable except blocks; treat that warning as a bug.
The Full try Shape (Most People Only Use Half)
try:
data = load(path)
except FileNotFoundError:
data = default()
else:
log.info("loaded") # runs only if NO exception occurred
finally:
cleanup() # runs no matter whatelse is the underused clause: code that should run only on success belongs there, not inside try — putting it in try makes its own exceptions look like they came from the guarded call, confusing your handlers.
Raising and Custom Exceptions
class RateLimitError(Exception):
def __init__(self, retry_after):
super().__init__(f"rate limited; retry in {retry_after}s")
self.retry_after = retry_after
raise RateLimitError(30)Two rules that keep exception design healthy: raise ValueError/TypeError for programming errors and let them propagate — do not convert them into silent defaults; use custom exceptions for domain conditions callers must handle (insufficient funds, rate limits). And when re-raising, use raise ... from err to keep the causal chain — bare raise inside except is fine (re-raises the active exception), but raise NewError() without from hides the original traceback.