Python Essentials for AI Engineer

Course Content

Python Essentials for AI Engineer

6 sections · 48 lessons

What are Custom Exceptions?


One base class, a different fix per failureexcept LLMErrorRateLimitError —wait retry_afterContextLengthError— trim historyContentFilterError— fallback textNew provider — same handlers
Callers branch on the class and read data from attributes, so a provider rewording its error message breaks nothing.

What you need to know

The minimum

Python
class EmptyIndexError(Exception):    """The vector index has no documents to search."""try:    raise EmptyIndexError("index 'faq-v3' has 0 documents")except EmptyIndexError as e:    print(e)          # index 'faq-v3' has 0 documents

A class body with just a docstring is enough. Names end in Error by convention.

Adding data

When the caller needs details to make a decision, store them as attributes and still pass a readable message to the parent:

Python
class RateLimitError(Exception):    def __init__(self, message, retry_after):        super().__init__(message)        self.retry_after = retry_aftertry:    raise RateLimitError("429 from provider", retry_after=12)except RateLimitError as e:    print(e, "| wait", e.retry_after, "s")     # 429 from provider | wait 12 s

A small hierarchy

Libraries define one base exception and a few subclasses: requests has RequestException, and the major LLM SDKs expose classes such as RateLimitError, APITimeoutError and BadRequestError under a common base. Callers can catch one specific case or the whole family.

When not to create one

If a built-in already says it — a bad argument value is a ValueError — use the built-in. Create custom classes for failures callers will want to handle differently, not one per function.

A real-life example

A team wraps several LLM providers behind one client. Callers should not care which provider failed; they need to know what kind of failure it was, because each needs a different fix:

Python
class LLMError(Exception):    """Base class for every failure from our LLM layer."""class RateLimitError(LLMError):    def __init__(self, retry_after):        super().__init__(f"rate limited, retry after {retry_after}s")        self.retry_after = retry_afterclass ContextLengthError(LLMError):    def __init__(self, tokens, limit):        super().__init__(f"{tokens} tokens exceeds limit of {limit}")        self.tokens, self.limit = tokens, limitclass ContentFilterError(LLMError):    passdef handle(err):    if isinstance(err, RateLimitError):        return f"sleep {err.retry_after}s and retry"    if isinstance(err, ContextLengthError):        return f"drop {err.tokens - err.limit} tokens of history and retry"    if isinstance(err, LLMError):        return "show a polite fallback message"for err in [RateLimitError(12), ContextLengthError(9200, 8192), ContentFilterError("blocked")]:    try:        raise err    except LLMError as e:        print(f"{type(e).__name__}: {handle(e)}")# RateLimitError: sleep 12s and retry# ContextLengthError: drop 1008 tokens of history and retry# ContentFilterError: show a polite fallback message

Each provider adapter translates its own HTTP errors into these classes. When the team adds a new provider, the retry, truncation and fallback logic does not change. And because everything inherits from LLMError, the web layer can catch LLMError once and never accidentally hide a genuine bug like a KeyError.

Follow-up questions to expect

  • "Why inherit from Exception and not BaseException?" — BaseException is for system-exiting events like KeyboardInterrupt. Code that does except Exception would not catch your error.
  • "Should custom exceptions carry extra data?" — Yes, when the caller needs it to decide what to do, such as retry_after or status_code. Always pass a readable message to super().__init__.
  • "How many custom exceptions should a project have?" — Few: one base class per library or layer, plus a subclass for each failure that callers handle differently.