Skip to content
Forge Learn/Errors & Robustness

Custom Exceptions & Defensive Coding Patterns

6 min read

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.

Defining and raising a custom exception

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`.

Two legitimate patterns for "not found" - pick deliberately

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.

assert for an internal invariant, exception for an expected failure
  • -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.
Check Yourself1 / 3

Why define a custom exception class instead of just raising a generic `Exception`?

ScaleDojo Logo
Initializing ScaleDojo