Custom Exceptions & Defensive Coding Patterns
You'll learn to
- -Define a custom exception by subclassing `Exception`
- -Decide when to raise an exception vs. return `None`/a sentinel value
- -Understand `assert` and how it differs from raising an exception for expected failures
Built-in exceptions like `ValueError` and `KeyError` cover generic cases, but well-designed code often defines its own exception types so that callers can catch exactly the failure they care about - and so the traceback itself documents what went wrong in domain terms, not generic ones.
Defining a Custom Exception
A custom exception is just a class that inherits from `Exception` (or one of its built-in subclasses). Most of the time, the body is nothing but `pass` - the value of a custom exception is its distinct *type*, which lets callers catch it specifically, not any special behavior inside it.
A caller can now catch `InsufficientFundsError` specifically, without accidentally also swallowing an unrelated `ValueError` from somewhere else in the same `try` block - the whole point of catching narrow, specific exception types from the previous chapter.
Raise vs. Return None vs. Sentinel Values
A recurring design decision: when an operation cannot produce a normal result, should the function raise an exception, or return something like `None` to signal "no result"? A useful rule of thumb: raise when the failure is exceptional and callers should be forced to notice it; return `None` (or another sentinel) when "not found" or "no result" is a completely normal, expected outcome that most callers will want to just check with an `if`.
This exact decision shows up constantly in Forge Algorithm Lab levels: a hash map or dictionary-like class whose spec says "raise KeyError if the key is not found" is asking for Pattern 1 above - explicitly and deliberately, because that is the behavior a real Python dict has too.
assert: Internal Invariants, Not Expected Failures
The `assert` statement checks a condition and raises `AssertionError` if it is false. It exists for internal invariants - things that should always be true if your own code is correct - not for validating external input or handling conditions you actually expect to occur, like a missing key or bad user input. The key distinction: an exception you `raise` deliberately is part of your function's documented behavior; an `assert` failing means there is a bug in your own logic.
- -Custom exceptions: subclass `Exception`, usually with just `pass` - the distinct type is the whole point.
- -Raise when a caller must be forced to notice and handle a failure; return `None`/a sentinel when "no result" is a normal, expected outcome.
- -`assert` is for catching your own bugs (internal invariants) during development - not a substitute for validating untrusted or expected-to-vary input.
- -Python can be run with optimizations that strip out `assert` statements entirely, which is another reason never to rely on them for real input validation.
Why define a custom exception class instead of just raising a generic `Exception`?